diff --git a/build.gradle b/build.gradle index 88324bc313b4..5a281121de80 100644 --- a/build.gradle +++ b/build.gradle @@ -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.32' apply false + id 'org.jetbrains.kotlin.jvm' version '1.5.0' 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' @@ -9,8 +9,9 @@ plugins { id "io.freefair.aspectj" version '5.1.1' apply false 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.32" apply false + id "me.champeau.jmh" version "0.6.4" apply false + id "org.jetbrains.kotlin.plugin.serialization" version "1.5.0" apply false + id "org.unbroken-dome.xjc" version '2.0.0' apply false } ext { @@ -26,14 +27,15 @@ configure(allprojects) { project -> dependencyManagement { imports { - mavenBom "com.fasterxml.jackson:jackson-bom:2.12.2" + mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3" mavenBom "io.netty:netty-bom:4.1.63.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.6" + mavenBom "io.projectreactor:reactor-bom:2020.0.7" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR9" mavenBom "io.rsocket:rsocket-bom:1.1.0" - mavenBom "org.eclipse.jetty:jetty-bom:9.4.39.v20210325" - mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.32" + mavenBom "org.eclipse.jetty:jetty-bom:9.4.40.v20210413" + mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.0" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.3" + mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.0" mavenBom "org.junit:junit-bom:5.7.1" } dependencies { @@ -54,7 +56,7 @@ configure(allprojects) { project -> entry 'aspectjtools' entry 'aspectjweaver' } - dependencySet(group: 'org.codehaus.groovy', version: '3.0.7') { + dependencySet(group: 'org.codehaus.groovy', version: '3.0.8') { entry 'groovy' entry 'groovy-jsr223' entry 'groovy-templates' // requires findbugs for warning-free compilation @@ -70,13 +72,13 @@ configure(allprojects) { project -> dependency "com.caucho:hessian:4.0.63" dependency "com.fasterxml:aalto-xml:1.2.2" - dependency("com.fasterxml.woodstox:woodstox-core:6.2.4") { + dependency("com.fasterxml.woodstox:woodstox-core:6.2.6") { exclude group: "stax", name: "stax-api" } dependency "com.google.code.gson:gson:2.8.6" - dependency "com.google.protobuf:protobuf-java-util:3.15.5" + dependency "com.google.protobuf:protobuf-java-util:3.15.8" dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4" - dependency("com.thoughtworks.xstream:xstream:1.4.15") { + dependency("com.thoughtworks.xstream:xstream:1.4.16") { exclude group: "xpp3", name: "xpp3_min" exclude group: "xmlpull", name: "xmlpull" } @@ -90,14 +92,10 @@ configure(allprojects) { project -> } dependency "org.ogce:xpp3:1.1.6" dependency "org.yaml:snakeyaml:1.28" - dependencySet(group: 'org.jetbrains.kotlinx', version: '1.0.1') { - entry 'kotlinx-serialization-core' - entry 'kotlinx-serialization-json' - } dependency "com.h2database:h2:1.4.200" - dependency "com.github.ben-manes.caffeine:caffeine:2.9.0" - dependency "com.github.librepdf:openpdf:1.3.25" + dependency "com.github.ben-manes.caffeine:caffeine:2.9.1" + dependency "com.github.librepdf:openpdf:1.3.26" dependency "com.rometools:rome:1.15.0" dependency "commons-io:commons-io:2.5" dependency "io.vavr:vavr:0.10.3" @@ -124,7 +122,7 @@ configure(allprojects) { project -> 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.30.Final" + dependency "org.hibernate:hibernate-core:5.4.31.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" @@ -198,7 +196,7 @@ configure(allprojects) { project -> exclude group: "org.hamcrest", name: "hamcrest-core" } } - dependencySet(group: 'org.mockito', version: '3.8.0') { + dependencySet(group: 'org.mockito', version: '3.9.0') { entry('mockito-core') { exclude group: "org.hamcrest", name: "hamcrest-core" } @@ -206,10 +204,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.10.2" - dependency("net.sourceforge.htmlunit:htmlunit:2.48.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.49.1") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.48.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.49.1") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -318,7 +316,7 @@ configure([rootProject] + javaProjects) { project -> kotlinOptions { languageVersion = "1.3" apiVersion = "1.3" - freeCompilerArgs = ["-Xjsr305=strict"] + freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings"] allWarningsAsErrors = true } } @@ -338,7 +336,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "8.41" + toolVersion = "8.42" configDirectory.set(rootProject.file("src/checkstyle")) } diff --git a/ci/parameters.yml b/ci/parameters.yml index 578a1b892998..a30e8561a224 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -5,7 +5,7 @@ github-repo: "https://github.com/spring-projects/spring-framework.git" github-repo-name: "spring-projects/spring-framework" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io" -branch: "master" +branch: "main" milestone: "5.3.x" build-name: "spring-framework" pipeline-name: "spring-framework" diff --git a/ci/pipeline.yml b/ci/pipeline.yml index e77e3b3990ae..eb752fe705cc 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -18,9 +18,6 @@ anchors: ARTIFACTORY_USERNAME: ((artifactory-username)) ARTIFACTORY_PASSWORD: ((artifactory-password)) build-project-task-params: &build-project-task-params - privileged: true - timeout: ((task-timeout)) - params: BRANCH: ((branch)) <<: *gradle-enterprise-task-params docker-resource-source: &docker-resource-source @@ -54,6 +51,11 @@ resource_types: source: repository: dpb587/github-status-resource tag: master +- name: pull-request + type: registry-image + source: + repository: teliaoss/github-pr-resource + tag: v0.23.0 - name: slack-notification type: registry-image source: @@ -93,6 +95,14 @@ resources: username: ((artifactory-username)) password: ((artifactory-password)) build_name: ((build-name)) +- name: git-pull-request + type: pull-request + icon: source-pull + source: + access_token: ((github-ci-pull-request-token)) + repository: ((github-repo-name)) + base_branch: ((branch)) + ignore_paths: ["ci/*"] - name: repo-status-build type: github-status-resource icon: eye-check-outline @@ -162,7 +172,10 @@ jobs: - task: build-project image: ci-image file: git-repo/ci/tasks/build-project.yml - <<: *build-project-task-params + privileged: true + timeout: ((task-timeout)) + params: + <<: *build-project-task-params on_failure: do: - put: repo-status-build @@ -217,10 +230,11 @@ jobs: - task: check-project image: ci-image file: git-repo/ci/tasks/check-project.yml + privileged: true + timeout: ((task-timeout)) params: - MAIN_TOOLCHAIN: 8 TEST_TOOLCHAIN: 11 - <<: *build-project-task-params + <<: *build-project-task-params on_failure: do: - put: repo-status-jdk11-build @@ -244,10 +258,11 @@ jobs: - task: check-project image: ci-image file: git-repo/ci/tasks/check-project.yml + privileged: true + timeout: ((task-timeout)) params: - MAIN_TOOLCHAIN: 8 TEST_TOOLCHAIN: 15 - <<: *build-project-task-params + <<: *build-project-task-params on_failure: do: - put: repo-status-jdk15-build @@ -257,6 +272,37 @@ jobs: <<: *slack-fail-params - put: repo-status-jdk15-build params: { state: "success", commit: "git-repo" } +- name: build-pull-requests + serial: true + public: true + plan: + - get: ci-image + - get: git-repo + resource: git-pull-request + trigger: true + version: every + - do: + - put: git-pull-request + params: + path: git-repo + status: pending + - task: build-pr + image: ci-image + file: git-repo/ci/tasks/build-pr.yml + privileged: true + timeout: ((task-timeout)) + params: + <<: *build-project-task-params + on_success: + put: git-pull-request + params: + path: git-repo + status: success + on_failure: + put: git-pull-request + params: + path: git-repo + status: failure - name: stage-milestone serial: true plan: @@ -412,3 +458,5 @@ groups: jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] - name: "ci-images" jobs: ["build-ci-images"] +- name: "pull-requests" + jobs: [ "build-pull-requests" ] diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh new file mode 100755 index 000000000000..94c4e8df65b4 --- /dev/null +++ b/ci/scripts/build-pr.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +source $(dirname $0)/common.sh + +pushd git-repo > /dev/null +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check +popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh index f2bf454e3597..7f6ca04cea9b 100755 --- a/ci/scripts/check-project.sh +++ b/ci/scripts/check-project.sh @@ -4,6 +4,6 @@ set -e source $(dirname $0)/common.sh pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Dorg.gradle.java.installations.fromEnv=JDK11,JDK15 \ - -PmainToolchain=$MAIN_TOOLCHAIN -PtestToolchain=$TEST_TOOLCHAIN --no-daemon --max-workers=4 check +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK11,JDK15 \ + -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check popd > /dev/null diff --git a/ci/tasks/build-pr.yml b/ci/tasks/build-pr.yml new file mode 100644 index 000000000000..dbf6e9c0cf60 --- /dev/null +++ b/ci/tasks/build-pr.yml @@ -0,0 +1,19 @@ +--- +platform: linux +inputs: +- name: git-repo +caches: +- path: gradle +params: + BRANCH: + CI: true + GRADLE_ENTERPRISE_ACCESS_KEY: + GRADLE_ENTERPRISE_CACHE_USERNAME: + GRADLE_ENTERPRISE_CACHE_PASSWORD: + GRADLE_ENTERPRISE_URL: https://ge.spring.io +run: + path: bash + args: + - -ec + - | + ${PWD}/git-repo/ci/scripts/build-pr.sh diff --git a/gradle.properties b/gradle.properties index 202ed82e194b..5c2e2f22cbd9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.6-SNAPSHOT +version=5.3.7 org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 48dea87a9b31..7db543d3bc4d 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -108,21 +108,3 @@ task eclipseBuildship(type: Copy) { tasks["eclipseJdt"].dependsOn(eclipseJdtPrepare) tasks["cleanEclipse"].dependsOn(cleanEclipseJdtUi) tasks["eclipse"].dependsOn(eclipseSettings, eclipseWstComponent) - - -// Filter 'build' folder -eclipse.project.file.withXml { - def node = it.asNode() - - def filteredResources = node.get("filteredResources") - if(filteredResources) { - node.remove(filteredResources) - } - def filterNode = node.appendNode("filteredResources").appendNode("filter") - filterNode.appendNode("id", "1359048889071") - filterNode.appendNode("name", "") - filterNode.appendNode("type", "30") - def matcherNode = filterNode.appendNode("matcher") - matcherNode.appendNode("id", "org.eclipse.ui.ide.multiFilter") - matcherNode.appendNode("arguments", "1.0-projectRelativePath-matches-false-false-build") -} diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 49efaeae84dc..e0faef367adb 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -4,12 +4,12 @@ apply plugin: 'org.springframework.build.optional-dependencies' // Uncomment the following for Shadow support in the jmhJar block. // Currently commented out due to ZipException: archive is not a ZIP archive // apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'me.champeau.gradle.jmh' +apply plugin: 'me.champeau.jmh' apply from: "$rootDir/gradle/publications.gradle" dependencies { - jmh 'org.openjdk.jmh:jmh-core:1.25' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.25' + jmh 'org.openjdk.jmh:jmh-core:1.28' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.28' jmh 'net.sf.jopt-simple:jopt-simple:4.6' } diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle index c6a61fe38414..5573efa1a52d 100644 --- a/gradle/toolchains.gradle +++ b/gradle/toolchains.gradle @@ -11,6 +11,8 @@ *
Expects the same syntax as Locale's {@code toString}, i.e. language + + *
Expects the same syntax as Locale's {@code toString()}, i.e. language + * optionally country + optionally variant, separated by "_" (e.g. "en", "en_US"). - * Also accepts spaces as separators, as alternative to underscores. + * Also accepts spaces as separators, as an alternative to underscores. * * @author Juergen Hoeller * @since 26.05.2003 diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java index 43e927f28306..519ead579879 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java @@ -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. @@ -44,8 +44,6 @@ public class PropertyComparator implements Comparator { private final SortDefinition sortDefinition; - private final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(false); - /** * Create a new PropertyComparator for the given SortDefinition. @@ -115,8 +113,9 @@ private Object getPropertyValue(Object obj) { // (similar to JSTL EL). If the property doesn't exist in the // first place, let the exception through. try { - this.beanWrapper.setWrappedInstance(obj); - return this.beanWrapper.getPropertyValue(this.sortDefinition.getProperty()); + BeanWrapperImpl beanWrapper = new BeanWrapperImpl(false); + beanWrapper.setWrappedInstance(obj); + return beanWrapper.getPropertyValue(this.sortDefinition.getProperty()); } catch (BeansException ex) { logger.debug("PropertyComparator could not access property - treating as null for sorting", ex); diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java index fcd0d63e12f4..6db5627ebcf7 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java @@ -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. @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -46,9 +45,6 @@ */ public class CandidateComponentsIndexer implements Processor { - private static final Set TYPE_KINDS = - Collections.unmodifiableSet(EnumSet.of(ElementKind.CLASS, ElementKind.INTERFACE)); - private MetadataStore metadataStore; private MetadataCollector metadataCollector; @@ -136,7 +132,8 @@ private void writeMetaData() { private static List staticTypesIn(Iterable extends Element> elements) { List list = new ArrayList<>(); for (Element element : elements) { - if (TYPE_KINDS.contains(element.getKind()) && element.getModifiers().contains(Modifier.STATIC)) { + if ((element.getKind().isClass() || element.getKind() == ElementKind.INTERFACE) && + element.getModifiers().contains(Modifier.STATIC) && element instanceof TypeElement) { list.add((TypeElement) element); } } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java index c8ef2b751851..dd2444706210 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java @@ -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. @@ -48,7 +48,7 @@ public IndexedStereotypesProvider(TypeHelper typeHelper) { public Set getStereotypes(Element element) { Set stereotypes = new LinkedHashSet<>(); ElementKind kind = element.getKind(); - if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + if (!kind.isClass() && kind != ElementKind.INTERFACE) { return stereotypes; } Set seen = new HashSet<>(); diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java index 068341965ad1..8a7137819d31 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java @@ -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. @@ -130,8 +130,8 @@ public void setShared(boolean shared) { @Override public void afterPropertiesSet() throws CacheException { - if (logger.isInfoEnabled()) { - logger.info("Initializing EhCache CacheManager" + + if (logger.isDebugEnabled()) { + logger.debug("Initializing EhCache CacheManager" + (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); } @@ -188,8 +188,8 @@ public boolean isSingleton() { @Override public void destroy() { if (this.cacheManager != null && this.locallyManaged) { - if (logger.isInfoEnabled()) { - logger.info("Shutting down EhCache CacheManager" + + if (logger.isDebugEnabled()) { + logger.debug("Shutting down EhCache CacheManager" + (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); } this.cacheManager.shutdown(); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 4d7d8564d29e..c528a83206bd 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -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. @@ -221,11 +221,11 @@ public void afterSingletonsInstantiated() { } catch (NoUniqueBeanDefinitionException ex) { throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " + - "CacheManager found. Mark one as primary or declare a specific CacheManager to use."); + "CacheManager found. Mark one as primary or declare a specific CacheManager to use.", ex); } catch (NoSuchBeanDefinitionException ex) { throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " + - "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration."); + "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.", ex); } } this.initialized = true; diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java index c8e24fa7c31f..79acbfaf2d94 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -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. @@ -57,17 +57,18 @@ public abstract class MessageSourceSupport { /** - * Set whether to always apply the {@code MessageFormat} rules, - * parsing even messages without arguments. - * Default is "false": Messages without arguments are by default - * returned as-is, without parsing them through MessageFormat. - * Set this to "true" to enforce MessageFormat for all messages, - * expecting all message texts to be written with MessageFormat escaping. - * For example, MessageFormat expects a single quote to be escaped - * as "''". If your message texts are all written with such escaping, - * even when not defining argument placeholders, you need to set this - * flag to "true". Else, only message texts with actual arguments - * are supposed to be written with MessageFormat escaping. + * Set whether to always apply the {@code MessageFormat} rules, parsing even + * messages without arguments. + * Default is {@code false}: Messages without arguments are by default + * returned as-is, without parsing them through {@code MessageFormat}. + * Set this to {@code true} to enforce {@code MessageFormat} for all messages, + * expecting all message texts to be written with {@code MessageFormat} escaping. + * For example, {@code MessageFormat} expects a single quote to be escaped + * as two adjacent single quotes ({@code "''"}). If your message texts are all + * written with such escaping, even when not defining argument placeholders, + * you need to set this flag to {@code true}. Otherwise, only message texts + * with actual arguments are supposed to be written with {@code MessageFormat} + * escaping. * @see java.text.MessageFormat */ public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { @@ -75,7 +76,7 @@ public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { } /** - * Return whether to always apply the MessageFormat rules, parsing even + * Return whether to always apply the {@code MessageFormat} rules, parsing even * messages without arguments. */ protected boolean isAlwaysUseMessageFormat() { @@ -150,10 +151,10 @@ protected String formatMessage(String msg, @Nullable Object[] args, Locale local } /** - * Create a MessageFormat for the given message and Locale. - * @param msg the message to create a MessageFormat for - * @param locale the Locale to create a MessageFormat for - * @return the MessageFormat instance + * Create a {@code MessageFormat} for the given message and Locale. + * @param msg the message to create a {@code MessageFormat} for + * @param locale the Locale to create a {@code MessageFormat} for + * @return the {@code MessageFormat} instance */ protected MessageFormat createMessageFormat(String msg, Locale locale) { return new MessageFormat(msg, locale); diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index 54168efb5fef..612728c5ad11 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -27,7 +27,7 @@ * * Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. * Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for - * millisecond timestamps) as well as JSR-310 {@code java.time} and Joda-Time value types. + * millisecond timestamps) as well as JSR-310 {@code java.time} value types. * * For style-based formatting, set the {@link #style} attribute to the desired style pattern code. * The first character of the code is the date style, and the second character is the time style. diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index d69e9c15e5fb..2158a4684fa9 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -219,9 +219,11 @@ public Date parse(String text, Locale locale) throws ParseException { } } if (this.source != null) { - throw new ParseException( + ParseException parseException = new ParseException( String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), ex.getErrorOffset()); + parseException.initCause(ex); + throw parseException; } // else rethrow original exception throw ex; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 2aaab8c4c590..559890ef5096 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -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. @@ -177,8 +177,8 @@ public void afterPropertiesSet() { * Set up the ExecutorService. */ public void initialize() { - if (logger.isInfoEnabled()) { - logger.info("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + if (logger.isDebugEnabled()) { + logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); } if (!this.threadNamePrefixSet && this.beanName != null) { setThreadNamePrefix(this.beanName + "-"); @@ -214,8 +214,8 @@ public void destroy() { * @see java.util.concurrent.ExecutorService#shutdownNow() */ public void shutdown() { - if (logger.isInfoEnabled()) { - logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + if (logger.isDebugEnabled()) { + logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); } if (this.executor != null) { if (this.waitForTasksToCompleteOnShutdown) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index d5dee884d6b0..0d9ac6ceffab 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -157,6 +157,11 @@ protected Type type() { return this.type; } + @SuppressWarnings("unchecked") + protected static > T cast(Temporal temporal) { + return (T) temporal; + } + /** * Represents the type of cron field, i.e. seconds, minutes, hours, @@ -236,16 +241,17 @@ public int checkValidValue(int value) { */ public > T elapseUntil(T temporal, int goal) { int current = get(temporal); + ValueRange range = temporal.range(this.field); if (current < goal) { - 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); + if (range.isValidIntValue(goal)) { + return cast(temporal.with(this.field, goal)); + } + else { + // goal is invalid, eg. 29th Feb, lets try to get as close as possible + return this.field.getBaseUnit().addTo(temporal, goal - current); } - return result; } else { - ValueRange range = temporal.range(this.field); long amount = goal + range.getMaximum() - current + 1 - range.getMinimum(); return this.field.getBaseUnit().addTo(temporal, amount); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index 8a3c5ba67e50..d656ab77fd6c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -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. @@ -326,12 +326,6 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } - @SuppressWarnings("unchecked") - private static > T cast(Temporal temporal) { - return (T) temporal; - } - - @Override public > T nextOrSame(T temporal) { T result = adjust(temporal); diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java index d5c2fa43ddb0..765a3fb1d62a 100644 --- a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -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. @@ -66,7 +66,8 @@ public ConcurrentModel(Object attributeValue) { @Override - public Object put(String key, Object value) { + @Nullable + public Object put(String key, @Nullable Object value) { if (value != null) { return super.put(key, value); } diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java new file mode 100644 index 000000000000..196f6fc6c74e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java @@ -0,0 +1,70 @@ +/* + * 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.validation.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; + +/** + * Utility class for handling validation annotations. + * Mainly for internal use within the framework. + * + * @author Christoph Dreis + * @since 5.3.7 + */ +public abstract class ValidationAnnotationUtils { + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** + * Determine any validation hints by the given annotation. + * This implementation checks for {@code @javax.validation.Valid}, + * Spring's {@link org.springframework.validation.annotation.Validated}, + * and custom annotations whose name starts with "Valid". + * @param ann the annotation (potentially a validation annotation) + * @return the validation hints to apply (possibly an empty array), + * or {@code null} if this annotation does not trigger any validation + */ + @Nullable + public static Object[] determineValidationHints(Annotation ann) { + Class extends Annotation> annotationType = ann.annotationType(); + String annotationName = annotationType.getName(); + if ("javax.validation.Valid".equals(annotationName)) { + return EMPTY_OBJECT_ARRAY; + } + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null) { + Object hints = validatedAnn.value(); + return convertValidationHints(hints); + } + if (annotationType.getSimpleName().startsWith("Valid")) { + Object hints = AnnotationUtils.getValue(ann); + return convertValidationHints(hints); + } + return null; + } + + private static Object[] convertValidationHints(@Nullable Object hints) { + if (hints == null) { + return EMPTY_OBJECT_ARRAY; + } + return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints}); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index fae93b5a59d1..ea7717478968 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -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. @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; @@ -87,6 +89,7 @@ public void multipleCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no unique bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoUniqueBeanDefinitionException.class); } } @@ -121,6 +124,7 @@ public void noCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoSuchBeanDefinitionException.class); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index ebfbc694dc51..77db53f069e0 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -119,6 +119,39 @@ void testBindDateAnnotated() { assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("10/31/09"); } + @Test + void styleDateWithInvalidFormat() { + String propertyName = "styleDate"; + String propertyValue = "99/01/01"; + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add(propertyName, propertyValue); + binder.bind(propertyValues); + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getErrorCount()).isEqualTo(1); + FieldError fieldError = bindingResult.getFieldError(propertyName); + TypeMismatchException exception = fieldError.unwrap(TypeMismatchException.class); + assertThat(exception) + .hasMessageContaining("for property 'styleDate'") + .hasCauseInstanceOf(ConversionFailedException.class).getCause() + .hasMessageContaining("for value '99/01/01'") + .hasCauseInstanceOf(IllegalArgumentException.class).getCause() + .hasMessageContaining("Parse attempt failed for value [99/01/01]") + .hasCauseInstanceOf(ParseException.class).getCause() + // Unable to parse date time value "99/01/01" using configuration from + // @org.springframework.format.annotation.DateTimeFormat(pattern=, style=S-, iso=NONE, fallbackPatterns=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. In addition, strings + // are enclosed in double quotes beginning with Java 9. Thus, we cannot check directly + // for the presence of "style=S-". + .hasMessageContainingAll( + "Unable to parse date time value \"99/01/01\" using configuration from", + "@org.springframework.format.annotation.DateTimeFormat", + "style=", "S-", "iso=NONE") + .hasCauseInstanceOf(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"99/01/01\"") + .hasNoCause(); + } + @Test void testBindDateArray() { MutablePropertyValues propertyValues = new MutablePropertyValues(); @@ -330,7 +363,10 @@ void patternDateWithUnsupportedPattern() { .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(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"210302\"") + .hasNoCause(); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 6aa28756f686..23a62770fdf3 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -339,10 +339,11 @@ void isoLocalDateWithInvalidFormat() { .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=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. .hasMessageContainingAll( "Unable to parse date time value \"2009-31-10\" using configuration from", - "@org.springframework.format.annotation.DateTimeFormat", - "iso=DATE", "fallbackPatterns=[]") + "@org.springframework.format.annotation.DateTimeFormat", "iso=DATE") .hasCauseInstanceOf(DateTimeParseException.class).getCause() .hasMessageStartingWith("Text '2009-31-10'") .hasCauseInstanceOf(DateTimeException.class).getCause() diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index aea49716d89e..b4457c9e09a2 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1276,6 +1276,14 @@ public void daylightSaving() { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 10 2 * * *"); + + last = ZonedDateTime.parse("2013-03-31T01:09:00+01:00[Europe/Amsterdam]"); + expected = ZonedDateTime.parse("2013-04-01T02:10:00+02:00[Europe/Amsterdam]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index 119b5bdbd278..1fe501b1301d 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -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. @@ -57,7 +57,7 @@ private void setUp(LocalDateTime localDateTime, TimeZone timeZone) { @ParameterizedCronTriggerTest - void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { + void matchAll(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -66,7 +66,7 @@ void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -76,7 +76,7 @@ void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -86,7 +86,7 @@ void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -98,7 +98,7 @@ void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -111,7 +111,7 @@ void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTim } @ParameterizedCronTriggerTest - void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -123,7 +123,7 @@ void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10-15 * * * * *", timeZone); @@ -134,7 +134,7 @@ void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 * * * * *", timeZone); @@ -152,7 +152,7 @@ void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 11 * * * *", timeZone); @@ -164,7 +164,7 @@ void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 10 * * * *", timeZone); @@ -177,7 +177,7 @@ void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -198,7 +198,7 @@ void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -220,7 +220,7 @@ void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -236,13 +236,13 @@ void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(2); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3); } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -257,7 +257,7 @@ void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -273,7 +273,7 @@ void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone ti } @ParameterizedCronTriggerTest - void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -294,7 +294,7 @@ void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -315,7 +315,7 @@ void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -336,7 +336,7 @@ void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZ } @ParameterizedCronTriggerTest - void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -357,7 +357,7 @@ void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -380,7 +380,7 @@ void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 31 * *", timeZone); @@ -396,7 +396,7 @@ void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -413,7 +413,7 @@ void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -429,7 +429,7 @@ void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -445,7 +445,7 @@ void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone tim } @ParameterizedCronTriggerTest - void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 5 * * * *", timeZone); @@ -459,12 +459,12 @@ void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.HOUR, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * 10 * * *", timeZone); @@ -479,12 +479,12 @@ void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* 5 10 * * *", timeZone); @@ -500,12 +500,12 @@ void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { // next trigger is in one second because second is wildcard this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * * 3 * *", timeZone); @@ -521,12 +521,12 @@ void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 3 11 *", timeZone); @@ -543,12 +543,12 @@ void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void nonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); // TODO: maybe try and detect this as a special case in parser? @@ -561,7 +561,7 @@ void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void leapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 29 2 *", timeZone); @@ -579,12 +579,12 @@ void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.YEAR, 4); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { + void weekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 7 ? * MON-FRI", timeZone); @@ -607,12 +607,12 @@ void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * 2 * *", timeZone); @@ -621,7 +621,7 @@ void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("57,59 * * * * *", timeZone); @@ -630,7 +630,7 @@ void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *", timeZone); @@ -639,7 +639,7 @@ void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void hourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *", timeZone); @@ -648,7 +648,7 @@ void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { + void dayNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0-6", timeZone); @@ -657,7 +657,7 @@ void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { + void sundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -666,7 +666,7 @@ void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { + void sundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -675,7 +675,7 @@ void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1-12 *", timeZone); @@ -684,7 +684,7 @@ void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 2 *", timeZone); @@ -693,91 +693,91 @@ void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testSecondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("44-77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 44-77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 27 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 23-28 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 28-45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 13 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 0 ?", timeZone)); } @ParameterizedCronTriggerTest - void testDayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 32 12 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * * 11-13 *", timeZone)); } @ParameterizedCronTriggerTest - void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { + void whitespace(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1 *", timeZone); @@ -786,7 +786,7 @@ void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { + void monthSequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 30 23 30 1/3 ?", timeZone); @@ -808,23 +808,33 @@ void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { // Next trigger is 3 months latter this.calendar.add(Calendar.MONTH, 3); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { + void daylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); - // This trigger has to be somewhere in between 2am and 3am + // This trigger has to be somewhere between 2:00 AM and 3:00 AM, so we + // use a cron expression for 2:10 AM every day. CronTrigger trigger = new CronTrigger("0 10 2 * * *", timeZone); + + // 2:00 AM on March 31, 2013: start of Daylight Saving Time for CET in 2013. + // Setting up last completion: + // - PST: Sun Mar 31 10:09:54 CEST 2013 + // - CET: Sun Mar 31 01:09:54 CET 2013 this.calendar.set(Calendar.DAY_OF_MONTH, 31); this.calendar.set(Calendar.MONTH, Calendar.MARCH); this.calendar.set(Calendar.YEAR, 2013); this.calendar.set(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.MINUTE, 9); this.calendar.set(Calendar.SECOND, 54); - Date localDate = this.calendar.getTime(); - TriggerContext context1 = getTriggerContext(localDate); + Date lastCompletionTime = this.calendar.getTime(); + + // Setting up expected next execution time: + // - PST: Sun Mar 31 11:10:00 CEST 2013 + // - CET: Mon Apr 01 02:10:00 CEST 2013 if (timeZone.equals(TimeZone.getTimeZone("CET"))) { // Clocks go forward an hour so 2am doesn't exist in CET for this localDateTime this.calendar.add(Calendar.DAY_OF_MONTH, 1); @@ -832,8 +842,10 @@ void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZon this.calendar.add(Calendar.HOUR_OF_DAY, 1); this.calendar.set(Calendar.MINUTE, 10); this.calendar.set(Calendar.SECOND, 0); - Object actual = localDate = trigger.nextExecutionTime(context1); - assertThat(actual).isEqualTo(this.calendar.getTime()); + + TriggerContext context = getTriggerContext(lastCompletionTime); + Object nextExecutionTime = trigger.nextExecutionTime(context); + assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } private static void roundup(Calendar calendar) { diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 4cec61e31bb4..f3af11b50a97 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -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. @@ -299,7 +299,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry, @Override @Nullable - public V remove(Object key) { + public V remove(@Nullable Object key) { return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable @@ -316,7 +316,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry) } @Override - public boolean remove(Object key, final Object value) { + public boolean remove(@Nullable Object key, final @Nullable Object value) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -333,7 +333,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - public boolean replace(K key, final V oldValue, final V newValue) { + public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -349,7 +349,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e @Override @Nullable - public V replace(K key, final V value) { + public V replace(@Nullable K key, final @Nullable V value) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index a3db322b6f63..4689d53ee1e6 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -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. @@ -211,7 +211,13 @@ public void putAll(Map extends String, ? extends V> map) { public V putIfAbsent(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.putIfAbsent(key, value); } @@ -221,7 +227,13 @@ public V putIfAbsent(String key, @Nullable V value) { public V computeIfAbsent(String key, Function super String, ? extends V> mappingFunction) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.computeIfAbsent(key, mappingFunction); } diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 0430128489c3..67871015cae2 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -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. @@ -68,7 +68,7 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str } return (str.length() >= firstIndex && - pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) && + pattern.startsWith(str.substring(0, firstIndex)) && simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); } diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index b17d6f85fda6..c35c0486025e 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -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. @@ -28,9 +28,11 @@ import org.springframework.lang.Nullable; /** - * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form - * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for - * user-supplied values. Values for substitution can be supplied using a {@link Properties} instance or + * Utility class for working with Strings that have placeholder values in them. + * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} + * these placeholders can be substituted for user-supplied values. + * + * Values for substitution can be supplied using a {@link Properties} instance or * using a {@link PlaceholderResolver}. * * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java index 3ec0b1b63004..80ac3bd3fcd8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -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. @@ -292,7 +292,7 @@ private void handleComment(Comment comment) throws SAXException { private void handleDtd(DTD dtd) throws SAXException { if (getLexicalHandler() != null) { - javax.xml.stream.Location location = dtd.getLocation(); + Location location = dtd.getLocation(); getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); } if (getLexicalHandler() != null) { diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt index c954a27592ef..e42228c717fd 100644 --- a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -34,7 +34,7 @@ operator fun PropertyResolver.get(key: String) : String? = getProperty(key) /** * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` - * variant returning a nullable [String]. + * variant returning a nullable `Foo`. * * @author Sebastien Deleuze * @since 5.1 diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java index 0a2f6df061bc..9f50d9d1e9e7 100644 --- a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -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. @@ -99,6 +99,12 @@ void computeIfAbsentWithExistingValue() { assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value3"); assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value3"); assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value3"); + + assertThat(map.put("null", null)).isNull(); + assertThat(map.putIfAbsent("NULL", "value")).isNull(); + assertThat(map.put("null", null)).isEqualTo("value"); + assertThat(map.computeIfAbsent("NULL", s -> "value")).isEqualTo("value"); + assertThat(map.get("null")).isEqualTo("value"); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index 7a682dbd4e58..43ae0324961c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -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. @@ -49,7 +49,7 @@ public abstract class AbstractExpressionTests { /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression * @param expectedResultType the expected class of the evaluation result @@ -106,15 +106,15 @@ public void evaluateAndAskForReturnType(String expression, Object expectedValue, /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * This method can also check if the expression is writable (for example, * it is a variable or property reference). * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression - * @param expectedClassOfResult the expected class of the evaluation result + * @param expectedResultType the expected class of the evaluation result * @param shouldBeWritable should the parsed expression be writable? */ - public void evaluate(String expression, Object expectedValue, Class> expectedClassOfResult, boolean shouldBeWritable) { + public void evaluate(String expression, Object expectedValue, Class> expectedResultType, boolean shouldBeWritable) { Expression expr = parser.parseExpression(expression); assertThat(expr).as("expression").isNotNull(); if (DEBUG) { @@ -134,7 +134,7 @@ public void evaluate(String expression, Object expectedValue, Class> expectedC else { assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); } - assertThat(expectedClassOfResult.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedClassOfResult + + assertThat(expectedResultType.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedResultType + "' but result was of type '" + resultType + "'").isTrue(); assertThat(expr.isWritable(context)).as("isWritable").isEqualTo(shouldBeWritable); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java index 148f31895b29..c7ce3cad9f55 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -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,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -40,98 +39,79 @@ * @author Sam Brannen * @author Juergen Hoeller */ -public class SelectionAndProjectionTests { +class SelectionAndProjectionTests { @Test - public void selectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInList() throws Exception { + void selectFirstItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInList() throws Exception { + void selectLastItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInSet() throws Exception { + void selectFirstItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInSet() throws Exception { + void selectLastItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new IterableTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectionWithArray() throws Exception { + void selectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -139,36 +119,29 @@ public void selectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInArray() throws Exception { + void selectFirstItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInArray() throws Exception { + void selectLastItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithPrimitiveArray() throws Exception { + void selectionWithPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -176,51 +149,41 @@ public void selectionWithPrimitiveArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInPrimitiveArray() throws Exception { + void selectFirstItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInPrimitiveArray() throws Exception { + void selectLastItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test @SuppressWarnings("unchecked") - public void selectionWithMap() { + void selectionWithMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("colors.?[key.startsWith('b')]"); Map colorsMap = (Map) exp.getValue(context); - assertThat(colorsMap.size()).isEqualTo(3); - assertThat(colorsMap.containsKey("beige")).isTrue(); - assertThat(colorsMap.containsKey("blue")).isTrue(); - assertThat(colorsMap.containsKey("brown")).isTrue(); + assertThat(colorsMap).containsOnlyKeys("beige", "blue", "brown"); } @Test @SuppressWarnings("unchecked") - public void selectFirstItemInMap() { + void selectFirstItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -232,7 +195,7 @@ public void selectFirstItemInMap() { @Test @SuppressWarnings("unchecked") - public void selectLastItemInMap() { + void selectLastItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -243,52 +206,43 @@ public void selectLastItemInMap() { } @Test - public void projectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createList()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createSet()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createIterable()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithArray() throws Exception { + void projectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testArray.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testArray", IntegerTestBean.createArray()); @@ -297,10 +251,7 @@ public void projectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Number.class); Number[] array = (Number[]) value; - assertThat(array.length).isEqualTo(3); - assertThat(array[0]).isEqualTo(5); - assertThat(array[1]).isEqualTo(5.9f); - assertThat(array[2]).isEqualTo(7); + assertThat(array).containsExactly(5, 5.9f, 7); } @@ -347,12 +298,7 @@ static class IterableTestBean { } public Iterable getIntegers() { - return new Iterable() { - @Override - public Iterator iterator() { - return integers.iterator(); - } - }; + return integers::iterator; } } @@ -429,12 +375,7 @@ static Set createSet() { static Iterable createIterable() { final Set set = createSet(); - return new Iterable() { - @Override - public Iterator iterator() { - return set.iterator(); - } - }; + return set::iterator; } static IntegerTestBean[] createArray() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java index fed0064aff70..ccec6462a355 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -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. @@ -31,15 +31,12 @@ * entry for each column, with the column name as key. * * The Map implementation to use and the key to use for each column - * in the column Map can be customized through overriding - * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * in the column Map can be customized by overriding {@link #createColumnMap} + * and {@link #getColumnKey}, respectively. * - * Note: By default, ColumnMapRowMapper will try to build a linked Map + * Note: By default, {@code ColumnMapRowMapper} will try to build a linked Map * with case-insensitive keys, to preserve column order as well as allow any - * casing to be used for column names. This requires Commons Collections on the - * classpath (which will be autodetected). Else, the fallback is a standard linked - * HashMap, which will still preserve column order but requires the application - * to specify the column names in the same casing as exposed by the driver. + * casing to be used for column names. * * @author Juergen Hoeller * @since 1.2 @@ -74,6 +71,7 @@ protected Map createColumnMap(int columnCount) { /** * Determine the key to use for the given column in the column Map. + * By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. * The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
Default is "false": Messages without arguments are by default - * returned as-is, without parsing them through MessageFormat. - * Set this to "true" to enforce MessageFormat for all messages, - * expecting all message texts to be written with MessageFormat escaping. - *
For example, MessageFormat expects a single quote to be escaped - * as "''". If your message texts are all written with such escaping, - * even when not defining argument placeholders, you need to set this - * flag to "true". Else, only message texts with actual arguments - * are supposed to be written with MessageFormat escaping. + * Set whether to always apply the {@code MessageFormat} rules, parsing even + * messages without arguments. + *
Default is {@code false}: Messages without arguments are by default + * returned as-is, without parsing them through {@code MessageFormat}. + * Set this to {@code true} to enforce {@code MessageFormat} for all messages, + * expecting all message texts to be written with {@code MessageFormat} escaping. + *
For example, {@code MessageFormat} expects a single quote to be escaped + * as two adjacent single quotes ({@code "''"}). If your message texts are all + * written with such escaping, even when not defining argument placeholders, + * you need to set this flag to {@code true}. Otherwise, only message texts + * with actual arguments are supposed to be written with {@code MessageFormat} + * escaping. * @see java.text.MessageFormat */ public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { @@ -75,7 +76,7 @@ public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { } /** - * Return whether to always apply the MessageFormat rules, parsing even + * Return whether to always apply the {@code MessageFormat} rules, parsing even * messages without arguments. */ protected boolean isAlwaysUseMessageFormat() { @@ -150,10 +151,10 @@ protected String formatMessage(String msg, @Nullable Object[] args, Locale local } /** - * Create a MessageFormat for the given message and Locale. - * @param msg the message to create a MessageFormat for - * @param locale the Locale to create a MessageFormat for - * @return the MessageFormat instance + * Create a {@code MessageFormat} for the given message and Locale. + * @param msg the message to create a {@code MessageFormat} for + * @param locale the Locale to create a {@code MessageFormat} for + * @return the {@code MessageFormat} instance */ protected MessageFormat createMessageFormat(String msg, Locale locale) { return new MessageFormat(msg, locale); diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index 54168efb5fef..612728c5ad11 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -27,7 +27,7 @@ * *
Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. * Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for - * millisecond timestamps) as well as JSR-310 {@code java.time} and Joda-Time value types. + * millisecond timestamps) as well as JSR-310 {@code java.time} value types. * *
For style-based formatting, set the {@link #style} attribute to the desired style pattern code. * The first character of the code is the date style, and the second character is the time style. diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index d69e9c15e5fb..2158a4684fa9 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -219,9 +219,11 @@ public Date parse(String text, Locale locale) throws ParseException { } } if (this.source != null) { - throw new ParseException( + ParseException parseException = new ParseException( String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), ex.getErrorOffset()); + parseException.initCause(ex); + throw parseException; } // else rethrow original exception throw ex; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 2aaab8c4c590..559890ef5096 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -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. @@ -177,8 +177,8 @@ public void afterPropertiesSet() { * Set up the ExecutorService. */ public void initialize() { - if (logger.isInfoEnabled()) { - logger.info("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + if (logger.isDebugEnabled()) { + logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); } if (!this.threadNamePrefixSet && this.beanName != null) { setThreadNamePrefix(this.beanName + "-"); @@ -214,8 +214,8 @@ public void destroy() { * @see java.util.concurrent.ExecutorService#shutdownNow() */ public void shutdown() { - if (logger.isInfoEnabled()) { - logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + if (logger.isDebugEnabled()) { + logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); } if (this.executor != null) { if (this.waitForTasksToCompleteOnShutdown) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index d5dee884d6b0..0d9ac6ceffab 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -157,6 +157,11 @@ protected Type type() { return this.type; } + @SuppressWarnings("unchecked") + protected static > T cast(Temporal temporal) { + return (T) temporal; + } + /** * Represents the type of cron field, i.e. seconds, minutes, hours, @@ -236,16 +241,17 @@ public int checkValidValue(int value) { */ public > T elapseUntil(T temporal, int goal) { int current = get(temporal); + ValueRange range = temporal.range(this.field); if (current < goal) { - 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); + if (range.isValidIntValue(goal)) { + return cast(temporal.with(this.field, goal)); + } + else { + // goal is invalid, eg. 29th Feb, lets try to get as close as possible + return this.field.getBaseUnit().addTo(temporal, goal - current); } - return result; } else { - ValueRange range = temporal.range(this.field); long amount = goal + range.getMaximum() - current + 1 - range.getMinimum(); return this.field.getBaseUnit().addTo(temporal, amount); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index 8a3c5ba67e50..d656ab77fd6c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -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. @@ -326,12 +326,6 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } - @SuppressWarnings("unchecked") - private static > T cast(Temporal temporal) { - return (T) temporal; - } - - @Override public > T nextOrSame(T temporal) { T result = adjust(temporal); diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java index d5c2fa43ddb0..765a3fb1d62a 100644 --- a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -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. @@ -66,7 +66,8 @@ public ConcurrentModel(Object attributeValue) { @Override - public Object put(String key, Object value) { + @Nullable + public Object put(String key, @Nullable Object value) { if (value != null) { return super.put(key, value); } diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java new file mode 100644 index 000000000000..196f6fc6c74e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java @@ -0,0 +1,70 @@ +/* + * 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.validation.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; + +/** + * Utility class for handling validation annotations. + * Mainly for internal use within the framework. + * + * @author Christoph Dreis + * @since 5.3.7 + */ +public abstract class ValidationAnnotationUtils { + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** + * Determine any validation hints by the given annotation. + * This implementation checks for {@code @javax.validation.Valid}, + * Spring's {@link org.springframework.validation.annotation.Validated}, + * and custom annotations whose name starts with "Valid". + * @param ann the annotation (potentially a validation annotation) + * @return the validation hints to apply (possibly an empty array), + * or {@code null} if this annotation does not trigger any validation + */ + @Nullable + public static Object[] determineValidationHints(Annotation ann) { + Class extends Annotation> annotationType = ann.annotationType(); + String annotationName = annotationType.getName(); + if ("javax.validation.Valid".equals(annotationName)) { + return EMPTY_OBJECT_ARRAY; + } + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null) { + Object hints = validatedAnn.value(); + return convertValidationHints(hints); + } + if (annotationType.getSimpleName().startsWith("Valid")) { + Object hints = AnnotationUtils.getValue(ann); + return convertValidationHints(hints); + } + return null; + } + + private static Object[] convertValidationHints(@Nullable Object hints) { + if (hints == null) { + return EMPTY_OBJECT_ARRAY; + } + return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints}); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index fae93b5a59d1..ea7717478968 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -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. @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; @@ -87,6 +89,7 @@ public void multipleCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no unique bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoUniqueBeanDefinitionException.class); } } @@ -121,6 +124,7 @@ public void noCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoSuchBeanDefinitionException.class); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index ebfbc694dc51..77db53f069e0 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -119,6 +119,39 @@ void testBindDateAnnotated() { assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("10/31/09"); } + @Test + void styleDateWithInvalidFormat() { + String propertyName = "styleDate"; + String propertyValue = "99/01/01"; + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add(propertyName, propertyValue); + binder.bind(propertyValues); + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getErrorCount()).isEqualTo(1); + FieldError fieldError = bindingResult.getFieldError(propertyName); + TypeMismatchException exception = fieldError.unwrap(TypeMismatchException.class); + assertThat(exception) + .hasMessageContaining("for property 'styleDate'") + .hasCauseInstanceOf(ConversionFailedException.class).getCause() + .hasMessageContaining("for value '99/01/01'") + .hasCauseInstanceOf(IllegalArgumentException.class).getCause() + .hasMessageContaining("Parse attempt failed for value [99/01/01]") + .hasCauseInstanceOf(ParseException.class).getCause() + // Unable to parse date time value "99/01/01" using configuration from + // @org.springframework.format.annotation.DateTimeFormat(pattern=, style=S-, iso=NONE, fallbackPatterns=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. In addition, strings + // are enclosed in double quotes beginning with Java 9. Thus, we cannot check directly + // for the presence of "style=S-". + .hasMessageContainingAll( + "Unable to parse date time value \"99/01/01\" using configuration from", + "@org.springframework.format.annotation.DateTimeFormat", + "style=", "S-", "iso=NONE") + .hasCauseInstanceOf(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"99/01/01\"") + .hasNoCause(); + } + @Test void testBindDateArray() { MutablePropertyValues propertyValues = new MutablePropertyValues(); @@ -330,7 +363,10 @@ void patternDateWithUnsupportedPattern() { .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(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"210302\"") + .hasNoCause(); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 6aa28756f686..23a62770fdf3 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -339,10 +339,11 @@ void isoLocalDateWithInvalidFormat() { .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=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. .hasMessageContainingAll( "Unable to parse date time value \"2009-31-10\" using configuration from", - "@org.springframework.format.annotation.DateTimeFormat", - "iso=DATE", "fallbackPatterns=[]") + "@org.springframework.format.annotation.DateTimeFormat", "iso=DATE") .hasCauseInstanceOf(DateTimeParseException.class).getCause() .hasMessageStartingWith("Text '2009-31-10'") .hasCauseInstanceOf(DateTimeException.class).getCause() diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index aea49716d89e..b4457c9e09a2 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1276,6 +1276,14 @@ public void daylightSaving() { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 10 2 * * *"); + + last = ZonedDateTime.parse("2013-03-31T01:09:00+01:00[Europe/Amsterdam]"); + expected = ZonedDateTime.parse("2013-04-01T02:10:00+02:00[Europe/Amsterdam]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index 119b5bdbd278..1fe501b1301d 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -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. @@ -57,7 +57,7 @@ private void setUp(LocalDateTime localDateTime, TimeZone timeZone) { @ParameterizedCronTriggerTest - void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { + void matchAll(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -66,7 +66,7 @@ void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -76,7 +76,7 @@ void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -86,7 +86,7 @@ void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -98,7 +98,7 @@ void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -111,7 +111,7 @@ void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTim } @ParameterizedCronTriggerTest - void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -123,7 +123,7 @@ void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10-15 * * * * *", timeZone); @@ -134,7 +134,7 @@ void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 * * * * *", timeZone); @@ -152,7 +152,7 @@ void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 11 * * * *", timeZone); @@ -164,7 +164,7 @@ void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 10 * * * *", timeZone); @@ -177,7 +177,7 @@ void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -198,7 +198,7 @@ void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -220,7 +220,7 @@ void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -236,13 +236,13 @@ void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(2); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3); } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -257,7 +257,7 @@ void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -273,7 +273,7 @@ void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone ti } @ParameterizedCronTriggerTest - void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -294,7 +294,7 @@ void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -315,7 +315,7 @@ void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -336,7 +336,7 @@ void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZ } @ParameterizedCronTriggerTest - void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -357,7 +357,7 @@ void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -380,7 +380,7 @@ void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 31 * *", timeZone); @@ -396,7 +396,7 @@ void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -413,7 +413,7 @@ void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -429,7 +429,7 @@ void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -445,7 +445,7 @@ void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone tim } @ParameterizedCronTriggerTest - void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 5 * * * *", timeZone); @@ -459,12 +459,12 @@ void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.HOUR, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * 10 * * *", timeZone); @@ -479,12 +479,12 @@ void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* 5 10 * * *", timeZone); @@ -500,12 +500,12 @@ void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { // next trigger is in one second because second is wildcard this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * * 3 * *", timeZone); @@ -521,12 +521,12 @@ void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 3 11 *", timeZone); @@ -543,12 +543,12 @@ void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void nonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); // TODO: maybe try and detect this as a special case in parser? @@ -561,7 +561,7 @@ void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void leapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 29 2 *", timeZone); @@ -579,12 +579,12 @@ void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.YEAR, 4); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { + void weekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 7 ? * MON-FRI", timeZone); @@ -607,12 +607,12 @@ void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * 2 * *", timeZone); @@ -621,7 +621,7 @@ void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("57,59 * * * * *", timeZone); @@ -630,7 +630,7 @@ void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *", timeZone); @@ -639,7 +639,7 @@ void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void hourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *", timeZone); @@ -648,7 +648,7 @@ void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { + void dayNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0-6", timeZone); @@ -657,7 +657,7 @@ void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { + void sundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -666,7 +666,7 @@ void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { + void sundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -675,7 +675,7 @@ void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1-12 *", timeZone); @@ -684,7 +684,7 @@ void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 2 *", timeZone); @@ -693,91 +693,91 @@ void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testSecondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("44-77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 44-77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 27 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 23-28 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 28-45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 13 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 0 ?", timeZone)); } @ParameterizedCronTriggerTest - void testDayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 32 12 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * * 11-13 *", timeZone)); } @ParameterizedCronTriggerTest - void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { + void whitespace(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1 *", timeZone); @@ -786,7 +786,7 @@ void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { + void monthSequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 30 23 30 1/3 ?", timeZone); @@ -808,23 +808,33 @@ void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { // Next trigger is 3 months latter this.calendar.add(Calendar.MONTH, 3); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { + void daylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); - // This trigger has to be somewhere in between 2am and 3am + // This trigger has to be somewhere between 2:00 AM and 3:00 AM, so we + // use a cron expression for 2:10 AM every day. CronTrigger trigger = new CronTrigger("0 10 2 * * *", timeZone); + + // 2:00 AM on March 31, 2013: start of Daylight Saving Time for CET in 2013. + // Setting up last completion: + // - PST: Sun Mar 31 10:09:54 CEST 2013 + // - CET: Sun Mar 31 01:09:54 CET 2013 this.calendar.set(Calendar.DAY_OF_MONTH, 31); this.calendar.set(Calendar.MONTH, Calendar.MARCH); this.calendar.set(Calendar.YEAR, 2013); this.calendar.set(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.MINUTE, 9); this.calendar.set(Calendar.SECOND, 54); - Date localDate = this.calendar.getTime(); - TriggerContext context1 = getTriggerContext(localDate); + Date lastCompletionTime = this.calendar.getTime(); + + // Setting up expected next execution time: + // - PST: Sun Mar 31 11:10:00 CEST 2013 + // - CET: Mon Apr 01 02:10:00 CEST 2013 if (timeZone.equals(TimeZone.getTimeZone("CET"))) { // Clocks go forward an hour so 2am doesn't exist in CET for this localDateTime this.calendar.add(Calendar.DAY_OF_MONTH, 1); @@ -832,8 +842,10 @@ void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZon this.calendar.add(Calendar.HOUR_OF_DAY, 1); this.calendar.set(Calendar.MINUTE, 10); this.calendar.set(Calendar.SECOND, 0); - Object actual = localDate = trigger.nextExecutionTime(context1); - assertThat(actual).isEqualTo(this.calendar.getTime()); + + TriggerContext context = getTriggerContext(lastCompletionTime); + Object nextExecutionTime = trigger.nextExecutionTime(context); + assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } private static void roundup(Calendar calendar) { diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 4cec61e31bb4..f3af11b50a97 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -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. @@ -299,7 +299,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry, @Override @Nullable - public V remove(Object key) { + public V remove(@Nullable Object key) { return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable @@ -316,7 +316,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry) } @Override - public boolean remove(Object key, final Object value) { + public boolean remove(@Nullable Object key, final @Nullable Object value) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -333,7 +333,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - public boolean replace(K key, final V oldValue, final V newValue) { + public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -349,7 +349,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e @Override @Nullable - public V replace(K key, final V value) { + public V replace(@Nullable K key, final @Nullable V value) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index a3db322b6f63..4689d53ee1e6 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -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. @@ -211,7 +211,13 @@ public void putAll(Map extends String, ? extends V> map) { public V putIfAbsent(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.putIfAbsent(key, value); } @@ -221,7 +227,13 @@ public V putIfAbsent(String key, @Nullable V value) { public V computeIfAbsent(String key, Function super String, ? extends V> mappingFunction) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.computeIfAbsent(key, mappingFunction); } diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 0430128489c3..67871015cae2 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -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. @@ -68,7 +68,7 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str } return (str.length() >= firstIndex && - pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) && + pattern.startsWith(str.substring(0, firstIndex)) && simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); } diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index b17d6f85fda6..c35c0486025e 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -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. @@ -28,9 +28,11 @@ import org.springframework.lang.Nullable; /** - * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form - * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for - * user-supplied values. Values for substitution can be supplied using a {@link Properties} instance or + * Utility class for working with Strings that have placeholder values in them. + * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} + * these placeholders can be substituted for user-supplied values. + * + * Values for substitution can be supplied using a {@link Properties} instance or * using a {@link PlaceholderResolver}. * * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java index 3ec0b1b63004..80ac3bd3fcd8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -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. @@ -292,7 +292,7 @@ private void handleComment(Comment comment) throws SAXException { private void handleDtd(DTD dtd) throws SAXException { if (getLexicalHandler() != null) { - javax.xml.stream.Location location = dtd.getLocation(); + Location location = dtd.getLocation(); getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); } if (getLexicalHandler() != null) { diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt index c954a27592ef..e42228c717fd 100644 --- a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -34,7 +34,7 @@ operator fun PropertyResolver.get(key: String) : String? = getProperty(key) /** * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` - * variant returning a nullable [String]. + * variant returning a nullable `Foo`. * * @author Sebastien Deleuze * @since 5.1 diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java index 0a2f6df061bc..9f50d9d1e9e7 100644 --- a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -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. @@ -99,6 +99,12 @@ void computeIfAbsentWithExistingValue() { assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value3"); assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value3"); assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value3"); + + assertThat(map.put("null", null)).isNull(); + assertThat(map.putIfAbsent("NULL", "value")).isNull(); + assertThat(map.put("null", null)).isEqualTo("value"); + assertThat(map.computeIfAbsent("NULL", s -> "value")).isEqualTo("value"); + assertThat(map.get("null")).isEqualTo("value"); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index 7a682dbd4e58..43ae0324961c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -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. @@ -49,7 +49,7 @@ public abstract class AbstractExpressionTests { /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression * @param expectedResultType the expected class of the evaluation result @@ -106,15 +106,15 @@ public void evaluateAndAskForReturnType(String expression, Object expectedValue, /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * This method can also check if the expression is writable (for example, * it is a variable or property reference). * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression - * @param expectedClassOfResult the expected class of the evaluation result + * @param expectedResultType the expected class of the evaluation result * @param shouldBeWritable should the parsed expression be writable? */ - public void evaluate(String expression, Object expectedValue, Class> expectedClassOfResult, boolean shouldBeWritable) { + public void evaluate(String expression, Object expectedValue, Class> expectedResultType, boolean shouldBeWritable) { Expression expr = parser.parseExpression(expression); assertThat(expr).as("expression").isNotNull(); if (DEBUG) { @@ -134,7 +134,7 @@ public void evaluate(String expression, Object expectedValue, Class> expectedC else { assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); } - assertThat(expectedClassOfResult.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedClassOfResult + + assertThat(expectedResultType.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedResultType + "' but result was of type '" + resultType + "'").isTrue(); assertThat(expr.isWritable(context)).as("isWritable").isEqualTo(shouldBeWritable); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java index 148f31895b29..c7ce3cad9f55 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -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,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -40,98 +39,79 @@ * @author Sam Brannen * @author Juergen Hoeller */ -public class SelectionAndProjectionTests { +class SelectionAndProjectionTests { @Test - public void selectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInList() throws Exception { + void selectFirstItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInList() throws Exception { + void selectLastItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInSet() throws Exception { + void selectFirstItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInSet() throws Exception { + void selectLastItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new IterableTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectionWithArray() throws Exception { + void selectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -139,36 +119,29 @@ public void selectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInArray() throws Exception { + void selectFirstItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInArray() throws Exception { + void selectLastItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithPrimitiveArray() throws Exception { + void selectionWithPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -176,51 +149,41 @@ public void selectionWithPrimitiveArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInPrimitiveArray() throws Exception { + void selectFirstItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInPrimitiveArray() throws Exception { + void selectLastItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test @SuppressWarnings("unchecked") - public void selectionWithMap() { + void selectionWithMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("colors.?[key.startsWith('b')]"); Map colorsMap = (Map) exp.getValue(context); - assertThat(colorsMap.size()).isEqualTo(3); - assertThat(colorsMap.containsKey("beige")).isTrue(); - assertThat(colorsMap.containsKey("blue")).isTrue(); - assertThat(colorsMap.containsKey("brown")).isTrue(); + assertThat(colorsMap).containsOnlyKeys("beige", "blue", "brown"); } @Test @SuppressWarnings("unchecked") - public void selectFirstItemInMap() { + void selectFirstItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -232,7 +195,7 @@ public void selectFirstItemInMap() { @Test @SuppressWarnings("unchecked") - public void selectLastItemInMap() { + void selectLastItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -243,52 +206,43 @@ public void selectLastItemInMap() { } @Test - public void projectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createList()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createSet()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createIterable()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithArray() throws Exception { + void projectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testArray.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testArray", IntegerTestBean.createArray()); @@ -297,10 +251,7 @@ public void projectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Number.class); Number[] array = (Number[]) value; - assertThat(array.length).isEqualTo(3); - assertThat(array[0]).isEqualTo(5); - assertThat(array[1]).isEqualTo(5.9f); - assertThat(array[2]).isEqualTo(7); + assertThat(array).containsExactly(5, 5.9f, 7); } @@ -347,12 +298,7 @@ static class IterableTestBean { } public Iterable getIntegers() { - return new Iterable() { - @Override - public Iterator iterator() { - return integers.iterator(); - } - }; + return integers::iterator; } } @@ -429,12 +375,7 @@ static Set createSet() { static Iterable createIterable() { final Set set = createSet(); - return new Iterable() { - @Override - public Iterator iterator() { - return set.iterator(); - } - }; + return set::iterator; } static IntegerTestBean[] createArray() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java index fed0064aff70..ccec6462a355 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -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. @@ -31,15 +31,12 @@ * entry for each column, with the column name as key. * * The Map implementation to use and the key to use for each column - * in the column Map can be customized through overriding - * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * in the column Map can be customized by overriding {@link #createColumnMap} + * and {@link #getColumnKey}, respectively. * - * Note: By default, ColumnMapRowMapper will try to build a linked Map + * Note: By default, {@code ColumnMapRowMapper} will try to build a linked Map * with case-insensitive keys, to preserve column order as well as allow any - * casing to be used for column names. This requires Commons Collections on the - * classpath (which will be autodetected). Else, the fallback is a standard linked - * HashMap, which will still preserve column order but requires the application - * to specify the column names in the same casing as exposed by the driver. + * casing to be used for column names. * * @author Juergen Hoeller * @since 1.2 @@ -74,6 +71,7 @@ protected Map createColumnMap(int columnCount) { /** * Determine the key to use for the given column in the column Map. + * By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. * The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
This implementation checks for {@code @javax.validation.Valid}, + * Spring's {@link org.springframework.validation.annotation.Validated}, + * and custom annotations whose name starts with "Valid". + * @param ann the annotation (potentially a validation annotation) + * @return the validation hints to apply (possibly an empty array), + * or {@code null} if this annotation does not trigger any validation + */ + @Nullable + public static Object[] determineValidationHints(Annotation ann) { + Class extends Annotation> annotationType = ann.annotationType(); + String annotationName = annotationType.getName(); + if ("javax.validation.Valid".equals(annotationName)) { + return EMPTY_OBJECT_ARRAY; + } + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validatedAnn != null) { + Object hints = validatedAnn.value(); + return convertValidationHints(hints); + } + if (annotationType.getSimpleName().startsWith("Valid")) { + Object hints = AnnotationUtils.getValue(ann); + return convertValidationHints(hints); + } + return null; + } + + private static Object[] convertValidationHints(@Nullable Object hints) { + if (hints == null) { + return EMPTY_OBJECT_ARRAY; + } + return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints}); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index fae93b5a59d1..ea7717478968 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -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. @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; @@ -87,6 +89,7 @@ public void multipleCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no unique bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoUniqueBeanDefinitionException.class); } } @@ -121,6 +124,7 @@ public void noCacheManagerBeans() { } catch (IllegalStateException ex) { assertThat(ex.getMessage().contains("no bean of type CacheManager")).isTrue(); + assertThat(ex).hasCauseInstanceOf(NoSuchBeanDefinitionException.class); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index ebfbc694dc51..77db53f069e0 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -119,6 +119,39 @@ void testBindDateAnnotated() { assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("10/31/09"); } + @Test + void styleDateWithInvalidFormat() { + String propertyName = "styleDate"; + String propertyValue = "99/01/01"; + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add(propertyName, propertyValue); + binder.bind(propertyValues); + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getErrorCount()).isEqualTo(1); + FieldError fieldError = bindingResult.getFieldError(propertyName); + TypeMismatchException exception = fieldError.unwrap(TypeMismatchException.class); + assertThat(exception) + .hasMessageContaining("for property 'styleDate'") + .hasCauseInstanceOf(ConversionFailedException.class).getCause() + .hasMessageContaining("for value '99/01/01'") + .hasCauseInstanceOf(IllegalArgumentException.class).getCause() + .hasMessageContaining("Parse attempt failed for value [99/01/01]") + .hasCauseInstanceOf(ParseException.class).getCause() + // Unable to parse date time value "99/01/01" using configuration from + // @org.springframework.format.annotation.DateTimeFormat(pattern=, style=S-, iso=NONE, fallbackPatterns=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. In addition, strings + // are enclosed in double quotes beginning with Java 9. Thus, we cannot check directly + // for the presence of "style=S-". + .hasMessageContainingAll( + "Unable to parse date time value \"99/01/01\" using configuration from", + "@org.springframework.format.annotation.DateTimeFormat", + "style=", "S-", "iso=NONE") + .hasCauseInstanceOf(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"99/01/01\"") + .hasNoCause(); + } + @Test void testBindDateArray() { MutablePropertyValues propertyValues = new MutablePropertyValues(); @@ -330,7 +363,10 @@ void patternDateWithUnsupportedPattern() { .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(ParseException.class).getCause() + .hasMessageStartingWith("Unparseable date: \"210302\"") + .hasNoCause(); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 6aa28756f686..23a62770fdf3 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -339,10 +339,11 @@ void isoLocalDateWithInvalidFormat() { .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=[]) + // We do not check "fallbackPatterns=[]", since the array representation in the toString() + // implementation for annotations changed from [] to {} in Java 9. .hasMessageContainingAll( "Unable to parse date time value \"2009-31-10\" using configuration from", - "@org.springframework.format.annotation.DateTimeFormat", - "iso=DATE", "fallbackPatterns=[]") + "@org.springframework.format.annotation.DateTimeFormat", "iso=DATE") .hasCauseInstanceOf(DateTimeParseException.class).getCause() .hasMessageStartingWith("Text '2009-31-10'") .hasCauseInstanceOf(DateTimeException.class).getCause() diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index aea49716d89e..b4457c9e09a2 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1276,6 +1276,14 @@ public void daylightSaving() { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 10 2 * * *"); + + last = ZonedDateTime.parse("2013-03-31T01:09:00+01:00[Europe/Amsterdam]"); + expected = ZonedDateTime.parse("2013-04-01T02:10:00+02:00[Europe/Amsterdam]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index 119b5bdbd278..1fe501b1301d 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -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. @@ -57,7 +57,7 @@ private void setUp(LocalDateTime localDateTime, TimeZone timeZone) { @ParameterizedCronTriggerTest - void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { + void matchAll(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -66,7 +66,7 @@ void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); @@ -76,7 +76,7 @@ void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void matchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -86,7 +86,7 @@ void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -98,7 +98,7 @@ void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); @@ -111,7 +111,7 @@ void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTim } @ParameterizedCronTriggerTest - void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); @@ -123,7 +123,7 @@ void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("10-15 * * * * *", timeZone); @@ -134,7 +134,7 @@ void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 * * * * *", timeZone); @@ -152,7 +152,7 @@ void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 11 * * * *", timeZone); @@ -164,7 +164,7 @@ void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 10 * * * *", timeZone); @@ -177,7 +177,7 @@ void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -198,7 +198,7 @@ void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); @@ -220,7 +220,7 @@ void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -236,13 +236,13 @@ void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(2); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3); } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -257,7 +257,7 @@ void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); @@ -273,7 +273,7 @@ void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone ti } @ParameterizedCronTriggerTest - void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -294,7 +294,7 @@ void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone } @ParameterizedCronTriggerTest - void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -315,7 +315,7 @@ void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { + void dailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); @@ -336,7 +336,7 @@ void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZ } @ParameterizedCronTriggerTest - void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -357,7 +357,7 @@ void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -380,7 +380,7 @@ void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 31 * *", timeZone); @@ -396,7 +396,7 @@ void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZon } @ParameterizedCronTriggerTest - void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + void monthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); @@ -413,7 +413,7 @@ void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -429,7 +429,7 @@ void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + void incrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); @@ -445,7 +445,7 @@ void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone tim } @ParameterizedCronTriggerTest - void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 5 * * * *", timeZone); @@ -459,12 +459,12 @@ void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.HOUR, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * 10 * * *", timeZone); @@ -479,12 +479,12 @@ void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { + void specificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* 5 10 * * *", timeZone); @@ -500,12 +500,12 @@ void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { // next trigger is in one second because second is wildcard this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("55 * * 3 * *", timeZone); @@ -521,12 +521,12 @@ void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.MINUTE, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void specificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("* * * 3 11 *", timeZone); @@ -543,12 +543,12 @@ void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.SECOND, 1); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void nonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); // TODO: maybe try and detect this as a special case in parser? @@ -561,7 +561,7 @@ void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) } @ParameterizedCronTriggerTest - void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + void leapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 0 29 2 *", timeZone); @@ -579,12 +579,12 @@ void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.YEAR, 4); TriggerContext context2 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context2); + Object actual = trigger.nextExecutionTime(context2); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { + void weekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 0 7 ? * MON-FRI", timeZone); @@ -607,12 +607,12 @@ void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { assertThat(actual1).isEqualTo(this.calendar.getTime()); this.calendar.add(Calendar.DAY_OF_MONTH, 1); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * 2 * *", timeZone); @@ -621,7 +621,7 @@ void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("57,59 * * * * *", timeZone); @@ -630,7 +630,7 @@ void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { + void secondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *", timeZone); @@ -639,7 +639,7 @@ void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZo } @ParameterizedCronTriggerTest - void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + void hourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *", timeZone); @@ -648,7 +648,7 @@ void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { + void dayNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0-6", timeZone); @@ -657,7 +657,7 @@ void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { + void sundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -666,7 +666,7 @@ void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { + void sundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); @@ -675,7 +675,7 @@ void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNames(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1-12 *", timeZone); @@ -684,7 +684,7 @@ void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { + void monthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 2 *", timeZone); @@ -693,91 +693,91 @@ void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testSecondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testSecondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void secondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("44-77 * * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testMinuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void minuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 44-77 * * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 27 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testHourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void hourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 23-28 * * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testDayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 28-45 * *", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 13 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { + void monthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 0 ?", timeZone)); } @ParameterizedCronTriggerTest - void testDayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void dayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 32 12 ?", timeZone)); } @ParameterizedCronTriggerTest - void testMonthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + void monthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * * 11-13 *", timeZone)); } @ParameterizedCronTriggerTest - void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { + void whitespace(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger1 = new CronTrigger("* * * * 1 *", timeZone); @@ -786,7 +786,7 @@ void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { } @ParameterizedCronTriggerTest - void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { + void monthSequence(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); CronTrigger trigger = new CronTrigger("0 30 23 30 1/3 ?", timeZone); @@ -808,23 +808,33 @@ void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { // Next trigger is 3 months latter this.calendar.add(Calendar.MONTH, 3); TriggerContext context3 = getTriggerContext(localDate); - Object actual = localDate = trigger.nextExecutionTime(context3); + Object actual = trigger.nextExecutionTime(context3); assertThat(actual).isEqualTo(this.calendar.getTime()); } @ParameterizedCronTriggerTest - void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { + void daylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { setUp(localDateTime, timeZone); - // This trigger has to be somewhere in between 2am and 3am + // This trigger has to be somewhere between 2:00 AM and 3:00 AM, so we + // use a cron expression for 2:10 AM every day. CronTrigger trigger = new CronTrigger("0 10 2 * * *", timeZone); + + // 2:00 AM on March 31, 2013: start of Daylight Saving Time for CET in 2013. + // Setting up last completion: + // - PST: Sun Mar 31 10:09:54 CEST 2013 + // - CET: Sun Mar 31 01:09:54 CET 2013 this.calendar.set(Calendar.DAY_OF_MONTH, 31); this.calendar.set(Calendar.MONTH, Calendar.MARCH); this.calendar.set(Calendar.YEAR, 2013); this.calendar.set(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.MINUTE, 9); this.calendar.set(Calendar.SECOND, 54); - Date localDate = this.calendar.getTime(); - TriggerContext context1 = getTriggerContext(localDate); + Date lastCompletionTime = this.calendar.getTime(); + + // Setting up expected next execution time: + // - PST: Sun Mar 31 11:10:00 CEST 2013 + // - CET: Mon Apr 01 02:10:00 CEST 2013 if (timeZone.equals(TimeZone.getTimeZone("CET"))) { // Clocks go forward an hour so 2am doesn't exist in CET for this localDateTime this.calendar.add(Calendar.DAY_OF_MONTH, 1); @@ -832,8 +842,10 @@ void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZon this.calendar.add(Calendar.HOUR_OF_DAY, 1); this.calendar.set(Calendar.MINUTE, 10); this.calendar.set(Calendar.SECOND, 0); - Object actual = localDate = trigger.nextExecutionTime(context1); - assertThat(actual).isEqualTo(this.calendar.getTime()); + + TriggerContext context = getTriggerContext(lastCompletionTime); + Object nextExecutionTime = trigger.nextExecutionTime(context); + assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } private static void roundup(Calendar calendar) { diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 4cec61e31bb4..f3af11b50a97 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -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. @@ -299,7 +299,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry, @Override @Nullable - public V remove(Object key) { + public V remove(@Nullable Object key) { return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable @@ -316,7 +316,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry) } @Override - public boolean remove(Object key, final Object value) { + public boolean remove(@Nullable Object key, final @Nullable Object value) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -333,7 +333,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - public boolean replace(K key, final V oldValue, final V newValue) { + public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -349,7 +349,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e @Override @Nullable - public V replace(K key, final V value) { + public V replace(@Nullable K key, final @Nullable V value) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override @Nullable diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index a3db322b6f63..4689d53ee1e6 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -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. @@ -211,7 +211,13 @@ public void putAll(Map extends String, ? extends V> map) { public V putIfAbsent(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.putIfAbsent(key, value); } @@ -221,7 +227,13 @@ public V putIfAbsent(String key, @Nullable V value) { public V computeIfAbsent(String key, Function super String, ? extends V> mappingFunction) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { - return this.targetMap.get(oldKey); + V oldKeyValue = this.targetMap.get(oldKey); + if (oldKeyValue != null) { + return oldKeyValue; + } + else { + key = oldKey; + } } return this.targetMap.computeIfAbsent(key, mappingFunction); } diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 0430128489c3..67871015cae2 100644 --- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -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. @@ -68,7 +68,7 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str } return (str.length() >= firstIndex && - pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) && + pattern.startsWith(str.substring(0, firstIndex)) && simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); } diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index b17d6f85fda6..c35c0486025e 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -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. @@ -28,9 +28,11 @@ import org.springframework.lang.Nullable; /** - * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form - * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for - * user-supplied values. Values for substitution can be supplied using a {@link Properties} instance or + * Utility class for working with Strings that have placeholder values in them. + * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} + * these placeholders can be substituted for user-supplied values. + * + * Values for substitution can be supplied using a {@link Properties} instance or * using a {@link PlaceholderResolver}. * * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java index 3ec0b1b63004..80ac3bd3fcd8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -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. @@ -292,7 +292,7 @@ private void handleComment(Comment comment) throws SAXException { private void handleDtd(DTD dtd) throws SAXException { if (getLexicalHandler() != null) { - javax.xml.stream.Location location = dtd.getLocation(); + Location location = dtd.getLocation(); getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); } if (getLexicalHandler() != null) { diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt index c954a27592ef..e42228c717fd 100644 --- a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -34,7 +34,7 @@ operator fun PropertyResolver.get(key: String) : String? = getProperty(key) /** * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` - * variant returning a nullable [String]. + * variant returning a nullable `Foo`. * * @author Sebastien Deleuze * @since 5.1 diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java index 0a2f6df061bc..9f50d9d1e9e7 100644 --- a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -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. @@ -99,6 +99,12 @@ void computeIfAbsentWithExistingValue() { assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value3"); assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value3"); assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value3"); + + assertThat(map.put("null", null)).isNull(); + assertThat(map.putIfAbsent("NULL", "value")).isNull(); + assertThat(map.put("null", null)).isEqualTo("value"); + assertThat(map.computeIfAbsent("NULL", s -> "value")).isEqualTo("value"); + assertThat(map.get("null")).isEqualTo("value"); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index 7a682dbd4e58..43ae0324961c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -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. @@ -49,7 +49,7 @@ public abstract class AbstractExpressionTests { /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression * @param expectedResultType the expected class of the evaluation result @@ -106,15 +106,15 @@ public void evaluateAndAskForReturnType(String expression, Object expectedValue, /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * This method can also check if the expression is writable (for example, * it is a variable or property reference). * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression - * @param expectedClassOfResult the expected class of the evaluation result + * @param expectedResultType the expected class of the evaluation result * @param shouldBeWritable should the parsed expression be writable? */ - public void evaluate(String expression, Object expectedValue, Class> expectedClassOfResult, boolean shouldBeWritable) { + public void evaluate(String expression, Object expectedValue, Class> expectedResultType, boolean shouldBeWritable) { Expression expr = parser.parseExpression(expression); assertThat(expr).as("expression").isNotNull(); if (DEBUG) { @@ -134,7 +134,7 @@ public void evaluate(String expression, Object expectedValue, Class> expectedC else { assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); } - assertThat(expectedClassOfResult.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedClassOfResult + + assertThat(expectedResultType.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedResultType + "' but result was of type '" + resultType + "'").isTrue(); assertThat(expr.isWritable(context)).as("isWritable").isEqualTo(shouldBeWritable); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java index 148f31895b29..c7ce3cad9f55 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -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,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -40,98 +39,79 @@ * @author Sam Brannen * @author Juergen Hoeller */ -public class SelectionAndProjectionTests { +class SelectionAndProjectionTests { @Test - public void selectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInList() throws Exception { + void selectFirstItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInList() throws Exception { + void selectLastItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInSet() throws Exception { + void selectFirstItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInSet() throws Exception { + void selectLastItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new IterableTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectionWithArray() throws Exception { + void selectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -139,36 +119,29 @@ public void selectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInArray() throws Exception { + void selectFirstItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInArray() throws Exception { + void selectLastItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithPrimitiveArray() throws Exception { + void selectionWithPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -176,51 +149,41 @@ public void selectionWithPrimitiveArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInPrimitiveArray() throws Exception { + void selectFirstItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInPrimitiveArray() throws Exception { + void selectLastItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test @SuppressWarnings("unchecked") - public void selectionWithMap() { + void selectionWithMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("colors.?[key.startsWith('b')]"); Map colorsMap = (Map) exp.getValue(context); - assertThat(colorsMap.size()).isEqualTo(3); - assertThat(colorsMap.containsKey("beige")).isTrue(); - assertThat(colorsMap.containsKey("blue")).isTrue(); - assertThat(colorsMap.containsKey("brown")).isTrue(); + assertThat(colorsMap).containsOnlyKeys("beige", "blue", "brown"); } @Test @SuppressWarnings("unchecked") - public void selectFirstItemInMap() { + void selectFirstItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -232,7 +195,7 @@ public void selectFirstItemInMap() { @Test @SuppressWarnings("unchecked") - public void selectLastItemInMap() { + void selectLastItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -243,52 +206,43 @@ public void selectLastItemInMap() { } @Test - public void projectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createList()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createSet()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createIterable()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithArray() throws Exception { + void projectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testArray.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testArray", IntegerTestBean.createArray()); @@ -297,10 +251,7 @@ public void projectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Number.class); Number[] array = (Number[]) value; - assertThat(array.length).isEqualTo(3); - assertThat(array[0]).isEqualTo(5); - assertThat(array[1]).isEqualTo(5.9f); - assertThat(array[2]).isEqualTo(7); + assertThat(array).containsExactly(5, 5.9f, 7); } @@ -347,12 +298,7 @@ static class IterableTestBean { } public Iterable getIntegers() { - return new Iterable() { - @Override - public Iterator iterator() { - return integers.iterator(); - } - }; + return integers::iterator; } } @@ -429,12 +375,7 @@ static Set createSet() { static Iterable createIterable() { final Set set = createSet(); - return new Iterable() { - @Override - public Iterator iterator() { - return set.iterator(); - } - }; + return set::iterator; } static IntegerTestBean[] createArray() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java index fed0064aff70..ccec6462a355 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -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. @@ -31,15 +31,12 @@ * entry for each column, with the column name as key. * * The Map implementation to use and the key to use for each column - * in the column Map can be customized through overriding - * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * in the column Map can be customized by overriding {@link #createColumnMap} + * and {@link #getColumnKey}, respectively. * - * Note: By default, ColumnMapRowMapper will try to build a linked Map + * Note: By default, {@code ColumnMapRowMapper} will try to build a linked Map * with case-insensitive keys, to preserve column order as well as allow any - * casing to be used for column names. This requires Commons Collections on the - * classpath (which will be autodetected). Else, the fallback is a standard linked - * HashMap, which will still preserve column order but requires the application - * to specify the column names in the same casing as exposed by the driver. + * casing to be used for column names. * * @author Juergen Hoeller * @since 1.2 @@ -74,6 +71,7 @@ protected Map createColumnMap(int columnCount) { /** * Determine the key to use for the given column in the column Map. + * By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. * The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
Values for substitution can be supplied using a {@link Properties} instance or + * Utility class for working with Strings that have placeholder values in them. + * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper} + * these placeholders can be substituted for user-supplied values. + * + *
Values for substitution can be supplied using a {@link Properties} instance or * using a {@link PlaceholderResolver}. * * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java index 3ec0b1b63004..80ac3bd3fcd8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -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. @@ -292,7 +292,7 @@ private void handleComment(Comment comment) throws SAXException { private void handleDtd(DTD dtd) throws SAXException { if (getLexicalHandler() != null) { - javax.xml.stream.Location location = dtd.getLocation(); + Location location = dtd.getLocation(); getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); } if (getLexicalHandler() != null) { diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt index c954a27592ef..e42228c717fd 100644 --- a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -34,7 +34,7 @@ operator fun PropertyResolver.get(key: String) : String? = getProperty(key) /** * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` - * variant returning a nullable [String]. + * variant returning a nullable `Foo`. * * @author Sebastien Deleuze * @since 5.1 diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java index 0a2f6df061bc..9f50d9d1e9e7 100644 --- a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -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. @@ -99,6 +99,12 @@ void computeIfAbsentWithExistingValue() { assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value3"); assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value3"); assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value3"); + + assertThat(map.put("null", null)).isNull(); + assertThat(map.putIfAbsent("NULL", "value")).isNull(); + assertThat(map.put("null", null)).isEqualTo("value"); + assertThat(map.computeIfAbsent("NULL", s -> "value")).isEqualTo("value"); + assertThat(map.get("null")).isEqualTo("value"); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java index 7a682dbd4e58..43ae0324961c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -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. @@ -49,7 +49,7 @@ public abstract class AbstractExpressionTests { /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression * @param expectedResultType the expected class of the evaluation result @@ -106,15 +106,15 @@ public void evaluateAndAskForReturnType(String expression, Object expectedValue, /** * Evaluate an expression and check that the actual result matches the - * expectedValue and the class of the result matches the expectedClassOfResult. + * expectedValue and the class of the result matches the expectedResultType. * This method can also check if the expression is writable (for example, * it is a variable or property reference). * @param expression the expression to evaluate * @param expectedValue the expected result for evaluating the expression - * @param expectedClassOfResult the expected class of the evaluation result + * @param expectedResultType the expected class of the evaluation result * @param shouldBeWritable should the parsed expression be writable? */ - public void evaluate(String expression, Object expectedValue, Class> expectedClassOfResult, boolean shouldBeWritable) { + public void evaluate(String expression, Object expectedValue, Class> expectedResultType, boolean shouldBeWritable) { Expression expr = parser.parseExpression(expression); assertThat(expr).as("expression").isNotNull(); if (DEBUG) { @@ -134,7 +134,7 @@ public void evaluate(String expression, Object expectedValue, Class> expectedC else { assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); } - assertThat(expectedClassOfResult.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedClassOfResult + + assertThat(expectedResultType.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedResultType + "' but result was of type '" + resultType + "'").isTrue(); assertThat(expr.isWritable(context)).as("isWritable").isEqualTo(shouldBeWritable); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java index 148f31895b29..c7ce3cad9f55 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -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,7 +17,6 @@ package org.springframework.expression.spel; import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -40,98 +39,79 @@ * @author Sam Brannen * @author Juergen Hoeller */ -public class SelectionAndProjectionTests { +class SelectionAndProjectionTests { @Test - public void selectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInList() throws Exception { + void selectFirstItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInList() throws Exception { + void selectLastItemInList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInSet() throws Exception { + void selectFirstItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInSet() throws Exception { + void selectLastItemInSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void selectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new IterableTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(5); - assertThat(list.get(0)).isEqualTo(0); - assertThat(list.get(1)).isEqualTo(1); - assertThat(list.get(2)).isEqualTo(2); - assertThat(list.get(3)).isEqualTo(3); - assertThat(list.get(4)).isEqualTo(4); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectionWithArray() throws Exception { + void selectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -139,36 +119,29 @@ public void selectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInArray() throws Exception { + void selectFirstItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInArray() throws Exception { + void selectLastItemInArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test - public void selectionWithPrimitiveArray() throws Exception { + void selectionWithPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.?[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); @@ -176,51 +149,41 @@ public void selectionWithPrimitiveArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); Integer[] array = (Integer[]) value; - assertThat(array.length).isEqualTo(5); - assertThat(array[0]).isEqualTo(0); - assertThat(array[1]).isEqualTo(1); - assertThat(array[2]).isEqualTo(2); - assertThat(array[3]).isEqualTo(3); - assertThat(array[4]).isEqualTo(4); + assertThat(array).containsExactly(0, 1, 2, 3, 4); } @Test - public void selectFirstItemInPrimitiveArray() throws Exception { + void selectFirstItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.^[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(0); } @Test - public void selectLastItemInPrimitiveArray() throws Exception { + void selectLastItemInPrimitiveArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("ints.$[#this<5]"); EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); Object value = expression.getValue(context); - boolean condition = value instanceof Integer; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Integer.class); assertThat(value).isEqualTo(4); } @Test @SuppressWarnings("unchecked") - public void selectionWithMap() { + void selectionWithMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("colors.?[key.startsWith('b')]"); Map colorsMap = (Map) exp.getValue(context); - assertThat(colorsMap.size()).isEqualTo(3); - assertThat(colorsMap.containsKey("beige")).isTrue(); - assertThat(colorsMap.containsKey("blue")).isTrue(); - assertThat(colorsMap.containsKey("brown")).isTrue(); + assertThat(colorsMap).containsOnlyKeys("beige", "blue", "brown"); } @Test @SuppressWarnings("unchecked") - public void selectFirstItemInMap() { + void selectFirstItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -232,7 +195,7 @@ public void selectFirstItemInMap() { @Test @SuppressWarnings("unchecked") - public void selectLastItemInMap() { + void selectLastItemInMap() { EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); ExpressionParser parser = new SpelExpressionParser(); @@ -243,52 +206,43 @@ public void selectLastItemInMap() { } @Test - public void projectionWithList() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithList() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createList()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithSet() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithSet() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createSet()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithIterable() throws Exception { + @SuppressWarnings("unchecked") + void projectionWithIterable() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testList", IntegerTestBean.createIterable()); Object value = expression.getValue(context); - boolean condition = value instanceof List; - assertThat(condition).isTrue(); - List> list = (List>) value; - assertThat(list.size()).isEqualTo(3); - assertThat(list.get(0)).isEqualTo(5); - assertThat(list.get(1)).isEqualTo(6); - assertThat(list.get(2)).isEqualTo(7); + assertThat(value).isInstanceOf(List.class); + List list = (List) value; + assertThat(list).containsExactly(5, 6, 7); } @Test - public void projectionWithArray() throws Exception { + void projectionWithArray() throws Exception { Expression expression = new SpelExpressionParser().parseRaw("#testArray.![wrapper.value]"); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("testArray", IntegerTestBean.createArray()); @@ -297,10 +251,7 @@ public void projectionWithArray() throws Exception { TypedValue typedValue = new TypedValue(value); assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Number.class); Number[] array = (Number[]) value; - assertThat(array.length).isEqualTo(3); - assertThat(array[0]).isEqualTo(5); - assertThat(array[1]).isEqualTo(5.9f); - assertThat(array[2]).isEqualTo(7); + assertThat(array).containsExactly(5, 5.9f, 7); } @@ -347,12 +298,7 @@ static class IterableTestBean { } public Iterable getIntegers() { - return new Iterable() { - @Override - public Iterator iterator() { - return integers.iterator(); - } - }; + return integers::iterator; } } @@ -429,12 +375,7 @@ static Set createSet() { static Iterable createIterable() { final Set set = createSet(); - return new Iterable() { - @Override - public Iterator iterator() { - return set.iterator(); - } - }; + return set::iterator; } static IntegerTestBean[] createArray() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java index fed0064aff70..ccec6462a355 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -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. @@ -31,15 +31,12 @@ * entry for each column, with the column name as key. * * The Map implementation to use and the key to use for each column - * in the column Map can be customized through overriding - * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * in the column Map can be customized by overriding {@link #createColumnMap} + * and {@link #getColumnKey}, respectively. * - * Note: By default, ColumnMapRowMapper will try to build a linked Map + * Note: By default, {@code ColumnMapRowMapper} will try to build a linked Map * with case-insensitive keys, to preserve column order as well as allow any - * casing to be used for column names. This requires Commons Collections on the - * classpath (which will be autodetected). Else, the fallback is a standard linked - * HashMap, which will still preserve column order but requires the application - * to specify the column names in the same casing as exposed by the driver. + * casing to be used for column names. * * @author Juergen Hoeller * @since 1.2 @@ -74,6 +71,7 @@ protected Map createColumnMap(int columnCount) { /** * Determine the key to use for the given column in the column Map. + * By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. * The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
The Map implementation to use and the key to use for each column - * in the column Map can be customized through overriding - * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * in the column Map can be customized by overriding {@link #createColumnMap} + * and {@link #getColumnKey}, respectively. * - *
Note: By default, ColumnMapRowMapper will try to build a linked Map + *
Note: By default, {@code ColumnMapRowMapper} will try to build a linked Map * with case-insensitive keys, to preserve column order as well as allow any - * casing to be used for column names. This requires Commons Collections on the - * classpath (which will be autodetected). Else, the fallback is a standard linked - * HashMap, which will still preserve column order but requires the application - * to specify the column names in the same casing as exposed by the driver. + * casing to be used for column names. * * @author Juergen Hoeller * @since 1.2 @@ -74,6 +71,7 @@ protected Map createColumnMap(int columnCount) { /** * Determine the key to use for the given column in the column Map. + * By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. * The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
By default, the supplied column name will be returned unmodified. * @param columnName the column name as returned by the ResultSet * @return the column key to use * @see java.sql.ResultSetMetaData#getColumnName @@ -86,9 +84,9 @@ protected String getColumnKey(String columnName) { * Retrieve a JDBC object value for the specified column. *
The default implementation uses the {@code getObject} method. * Additionally, this implementation includes a "hack" to get around Oracle - * returning a non standard object for their TIMESTAMP datatype. - * @param rs is the ResultSet holding the data - * @param index is the column index + * returning a non standard object for their TIMESTAMP data type. + * @param rs the ResultSet holding the data + * @param index the column index * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 0cecdc530f1a..6783441fce7b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -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,7 +22,9 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; +import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +52,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private String[] constructorParameterNames; @Nullable - private Class>[] constructorParameterTypes; + private TypeDescriptor[] constructorParameterTypes; /** @@ -75,9 +77,13 @@ protected void initialize(Class mappedClass) { super.initialize(mappedClass); this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); - if (this.mappedConstructor.getParameterCount() > 0) { + int paramCount = this.mappedConstructor.getParameterCount(); + if (paramCount > 0) { this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); - this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + this.constructorParameterTypes = new TypeDescriptor[paramCount]; + for (int i = 0; i < paramCount; i++) { + this.constructorParameterTypes[i] = new TypeDescriptor(new MethodParameter(this.mappedConstructor, i)); + } } } @@ -90,8 +96,9 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = underscoreName(this.constructorParameterNames[i]); - Class> type = this.constructorParameterTypes[i]; - args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + TypeDescriptor td = this.constructorParameterTypes[i]; + Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + args[i] = tc.convertIfNecessary(value, td.getType(), td); } } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java index cf6d0f04146a..bc00b8d925f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -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. @@ -40,22 +40,27 @@ * * Example: * - * create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
Example: * - *
create table tab (id int unsigned not null primary key, text varchar(100)); + * + * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0); * - * If "cacheSize" is set, the intermediate values are served without querying the + * If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * * It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + * As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
+ * create table tab (id int unsigned not null primary key, text varchar(100)); * create table tab_sequence (value int not null); * insert into tab_sequence values(0);
If {@code cacheSize} is set, the intermediate values are served without querying the * database. If the server or your application is stopped or crashes or a transaction * is rolled back, the unused values will never be served. The maximum hole size in - * numbering is consequently the value of cacheSize. + * numbering is consequently the value of {@code cacheSize}. * *
It is possible to avoid acquiring a new connection for the incrementer by setting the * "useNewConnection" property to false. In this case you MUST use a non-transactional * storage engine like MYISAM when defining the incrementer table. * + *
As of Spring Framework 5.3.7, {@code MySQLMaxValueIncrementer} is compatible with + * MySQL safe updates mode. + * * @author Jean-Pierre Pawlak * @author Thomas Risberg * @author Juergen Hoeller + * @author Sam Brannen */ public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { @@ -141,7 +146,7 @@ protected synchronized long getNextKey() throws DataAccessException { String columnName = getColumnName(); try { stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + - " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + " = last_insert_id(" + columnName + " + " + getCacheSize() + ") limit 1"); } catch (SQLException ex) { throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 93716e5e9d03..601bbdfd7a1d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -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,6 +135,7 @@ public Mock(MockType type) throws Exception { given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.getObject(4)).willReturn(new BigDecimal("1234.56")); given(resultSet.wasNull()).willReturn(type == MockType.TWO); given(resultSetMetaData.getColumnCount()).willReturn(4); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index bc2cae0f40e8..473cb6f14c83 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -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,11 +16,15 @@ package org.springframework.jdbc.core; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.ConstructorPersonWithGenerics; import static org.assertj.core.api.Assertions.assertThat; @@ -42,4 +46,20 @@ public void testStaticQueryWithDataClass() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataClassAndGenerics() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPersonWithGenerics.class)); + assertThat(result.size()).isEqualTo(1); + ConstructorPersonWithGenerics person = result.get(0); + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); + + mock.verifyClosed(); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java index 0e15987af632..53f726d3a071 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -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. @@ -24,13 +24,13 @@ */ public class ConstructorPerson { - private String name; + private final String name; - private long age; + private final long age; - private java.util.Date birth_date; + private final Date birth_date; - private BigDecimal balance; + private final BigDecimal balance; public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { @@ -42,19 +42,19 @@ public ConstructorPerson(String name, long age, Date birth_date, BigDecimal bala public String name() { - return name; + return this.name; } public long age() { - return age; + return this.age; } public Date birth_date() { - return birth_date; + return this.birth_date; } public BigDecimal balance() { - return balance; + return this.balance; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java new file mode 100644 index 000000000000..3ae8e271c810 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -0,0 +1,61 @@ +/* + * 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.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPersonWithGenerics { + + private final String name; + + private final long age; + + private final Date birth_date; + + private final List balance; + + + public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return this.name; + } + + public long age() { + return this.age; + } + + public Date birth_date() { + return this.birth_date; + } + + public List balance() { + return this.balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java index d2e3594abe44..7cbb99047bd8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -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. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; @@ -38,10 +39,13 @@ import static org.mockito.Mockito.verify; /** + * Unit tests for {@link DataFieldMaxValueIncrementer} implementations. + * * @author Juergen Hoeller + * @author Sam Brannen * @since 27.02.2004 */ -public class DataFieldMaxValueIncrementerTests { +class DataFieldMaxValueIncrementerTests { private final DataSource dataSource = mock(DataSource.class); @@ -53,7 +57,7 @@ public class DataFieldMaxValueIncrementerTests { @Test - public void testHanaSequenceMaxValueIncrementer() throws SQLException { + void hanaSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); @@ -75,7 +79,7 @@ public void testHanaSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementer() throws SQLException { + void hsqlMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -105,7 +109,7 @@ public void testHsqlMaxValueIncrementer() throws SQLException { } @Test - public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + void hsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); @@ -136,7 +140,7 @@ public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLExce } @Test - public void testMySQLMaxValueIncrementer() throws SQLException { + void mySQLMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); @@ -156,14 +160,14 @@ public void testMySQLMaxValueIncrementer() throws SQLException { assertThat(incrementer.nextStringValue()).isEqualTo("3"); assertThat(incrementer.nextLongValue()).isEqualTo(4); - verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2) limit 1"); verify(resultSet, times(2)).close(); verify(statement, times(2)).close(); verify(connection, times(2)).close(); } @Test - public void testOracleSequenceMaxValueIncrementer() throws SQLException { + void oracleSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); @@ -185,7 +189,7 @@ public void testOracleSequenceMaxValueIncrementer() throws SQLException { } @Test - public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + void postgresSequenceMaxValueIncrementer() throws SQLException { given(dataSource.getConnection()).willReturn(connection); given(connection.createStatement()).willReturn(statement); given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 22d827b38f50..d0a19fa5cf6b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -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. @@ -179,6 +179,23 @@ public boolean isCacheConsumers() { } + /** + * Return a current session count, indicating the number of sessions currently + * cached by this connection factory. + * @since 5.3.7 + */ + public int getCachedSessionCount() { + int count = 0; + synchronized (this.cachedSessions) { + for (Deque sessionList : this.cachedSessions.values()) { + synchronized (sessionList) { + count += sessionList.size(); + } + } + } + return count; + } + /** * Resets the Session cache as well. */ diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java index a3995e8a6e26..63c726037734 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketRequester.java @@ -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. @@ -30,6 +30,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import org.reactivestreams.Publisher; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +50,7 @@ * @author Brian Clozel * @since 5.2 */ -public interface RSocketRequester { +public interface RSocketRequester extends Disposable { /** * Return the underlying {@link RSocketClient} used to make requests with. @@ -110,6 +111,27 @@ public interface RSocketRequester { */ RequestSpec metadata(Object metadata, @Nullable MimeType mimeType); + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()} in order to close the connection from the + * underlying transport and notify subscribers. + * @since 5.3.7 + */ + @Override + default void dispose() { + rsocketClient().dispose(); + } + + /** + * Shortcut method that delegates to the same on the underlying + * {@link #rsocketClient()}. + * @since 5.3.7 + */ + @Override + default boolean isDisposed() { + return rsocketClient().isDisposed(); + } + /** * Obtain a builder to create a client {@link RSocketRequester} by connecting * to an RSocket server. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index f4f8ebe90007..37c2d3b40022 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -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. @@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration { private final List destinationPrefixes; + /** + * Create a new broker registration. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { - Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null"); + Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); this.clientInboundChannel = clientInboundChannel; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 4c11e6845523..68e60f691b5a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -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. @@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { private String selectorHeaderName = "selector"; - public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) { - super(inChannel, outChannel, prefixes); + /** + * Create a new {@code SimpleBrokerRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ + public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel, + MessageChannel clientOutboundChannel, String[] destinationPrefixes) { + + super(clientInboundChannel, clientOutboundChannel, destinationPrefixes); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index d24b63e2dd01..526c4cf4fd73 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -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. @@ -68,6 +68,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String userRegistryBroadcast; + /** + * Create a new {@code StompBrokerRelayRegistration}. + * @param clientInboundChannel the inbound channel + * @param clientOutboundChannel the outbound channel + * @param destinationPrefixes the destination prefixes + */ public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel, MessageChannel clientOutboundChannel, String[] destinationPrefixes) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 45e78feeff06..cd0143a2cfe1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -166,7 +166,10 @@ private StubArgumentResolver getStubResolver(int index) { @SuppressWarnings("unused") - private static class Handler { + static class Handler { + + public Handler() { + } public String handle(Integer intArg, String stringArg) { return intArg + "-" + stringArg; @@ -181,7 +184,7 @@ public void handleWithException(Throwable ex) throws Throwable { } - private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { + static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 3f19a54ada93..ead73327bb90 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -183,6 +183,8 @@ private static class Handler { private AtomicReference result = new AtomicReference<>(); + public Handler() { + } public String getResult() { return this.result.get(); diff --git a/spring-oxm/spring-oxm.gradle b/spring-oxm/spring-oxm.gradle index 9d23276d2282..ff0c8abbc88e 100644 --- a/spring-oxm/spring-oxm.gradle +++ b/spring-oxm/spring-oxm.gradle @@ -1,56 +1,24 @@ +plugins { + id "org.unbroken-dome.xjc" +} + description = "Spring Object/XML Marshalling" configurations { jibx - xjc } dependencies { jibx "org.jibx:jibx-bind:1.3.3" jibx "org.apache.bcel:bcel:6.0" - xjc "javax.xml.bind:jaxb-api:2.3.1" - xjc "com.sun.xml.bind:jaxb-core:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-impl:2.3.0.1" - xjc "com.sun.xml.bind:jaxb-xjc:2.3.1" - xjc "com.sun.activation:javax.activation:1.2.0" } -ext.genSourcesDir = "${buildDir}/generated-sources" -ext.flightSchema = "${projectDir}/src/test/resources/org/springframework/oxm/flight.xsd" - -task genJaxb { - ext.sourcesDir = "${genSourcesDir}/jaxb" - ext.classesDir = "${buildDir}/classes/jaxb" - - inputs.files(flightSchema).withPathSensitivity(PathSensitivity.RELATIVE) - outputs.dir classesDir - - doLast() { - project.ant { - taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", - classpath: configurations.xjc.asPath - mkdir(dir: sourcesDir) - mkdir(dir: classesDir) - - xjc(destdir: sourcesDir, schema: flightSchema, - package: "org.springframework.oxm.jaxb.test") { - produces(dir: sourcesDir, includes: "**/*.java") - } - - javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true, - debugLevel: "lines,vars,source", - classpath: configurations.xjc.asPath) { - src(path: sourcesDir) - include(name: "**/*.java") - include(name: "*.java") - } - - copy(todir: classesDir) { - fileset(dir: sourcesDir, erroronmissingdir: false) { - exclude(name: "**/*.java") - } - } - } +xjc { + xjcVersion = '2.2' +} +sourceSets { + test { + xjcTargetPackage = 'org.springframework.oxm.jaxb.test' } } @@ -67,7 +35,7 @@ dependencies { testCompile("org.codehaus.jettison:jettison") { exclude group: "stax", module: "stax-api" } - testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) + //testCompile(files(genJaxb.classesDir).builtBy(genJaxb)) testCompile("org.xmlunit:xmlunit-assertj") testCompile("org.xmlunit:xmlunit-matchers") testRuntime("com.sun.xml.bind:jaxb-core") @@ -76,7 +44,7 @@ dependencies { // JiBX compiler is currently not compatible with JDK 9+. // If customJavaHome has been set, we assume the custom JDK version is 9+. -if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !System.getProperty("customJavaSourceVersion")) { +if ((JavaVersion.current() == JavaVersion.VERSION_1_8) && !project.hasProperty("testToolchain")) { compileTestJava { def bindingXml = "${projectDir}/src/test/resources/org/springframework/oxm/jibx/binding.xml" diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index be10b7fecdb9..a0e88fef2689 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -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. @@ -78,7 +78,7 @@ * @author Biju Kunjummen * @author Sam Brannen */ -public class Jaxb2MarshallerTests extends AbstractMarshallerTests { +class Jaxb2MarshallerTests extends AbstractMarshallerTests { private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test"; @@ -104,7 +104,7 @@ protected Object createFlights() { @Test - public void marshalSAXResult() throws Exception { + void marshalSAXResult() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); SAXResult result = new SAXResult(contentHandler); marshaller.marshal(flights, result); @@ -124,7 +124,7 @@ public void marshalSAXResult() throws Exception { } @Test - public void lazyInit() throws Exception { + void lazyInit() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setLazyInit(true); @@ -137,48 +137,44 @@ public void lazyInit() throws Exception { } @Test - public void properties() throws Exception { + void properties() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath(CONTEXT_PATH); marshaller.setMarshallerProperties( - Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, - Boolean.TRUE)); + Collections.singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE)); marshaller.afterPropertiesSet(); } @Test - public void noContextPathOrClassesToBeBound() throws Exception { + void noContextPathOrClassesToBeBound() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); - assertThatIllegalArgumentException().isThrownBy( - marshaller::afterPropertiesSet); + assertThatIllegalArgumentException().isThrownBy(marshaller::afterPropertiesSet); } @Test - public void testInvalidContextPath() throws Exception { + void testInvalidContextPath() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("ab"); - assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy( - marshaller::afterPropertiesSet); + assertThatExceptionOfType(UncategorizedMappingException.class).isThrownBy(marshaller::afterPropertiesSet); } @Test - public void marshalInvalidClass() throws Exception { + void marshalInvalidClass() throws Exception { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(FlightType.class); marshaller.afterPropertiesSet(); Result result = new StreamResult(new StringWriter()); Flights flights = new Flights(); - assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> - marshaller.marshal(flights, result)); + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> marshaller.marshal(flights, result)); } @Test - public void supportsContextPath() throws Exception { + void supportsContextPath() throws Exception { testSupports(); } @Test - public void supportsClassesToBeBound() throws Exception { + void supportsClassesToBeBound() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Flights.class, FlightType.class); marshaller.afterPropertiesSet(); @@ -186,7 +182,7 @@ public void supportsClassesToBeBound() throws Exception { } @Test - public void supportsPackagesToScan() throws Exception { + void supportsPackagesToScan() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan(CONTEXT_PATH); marshaller.afterPropertiesSet(); @@ -224,11 +220,11 @@ private void testSupports() throws Exception { private void testSupportsPrimitives() { final Primitives primitives = new Primitives(); - ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(Primitives.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(primitives); @@ -237,22 +233,18 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("primitive"); - } - }); + }, + method -> method.getName().startsWith("primitive") + ); } private void testSupportsStandardClasses() throws Exception { final StandardClasses standardClasses = new StandardClasses(); - ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.doWithMethods(StandardClasses.class, method -> { Type returnType = method.getGenericReturnType(); - assertThat(marshaller.supports(returnType)).as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">").isTrue(); + assertThat(marshaller.supports(returnType)) + .as("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">") + .isTrue(); try { // make sure the marshalling does not result in errors Object returnValue = method.invoke(standardClasses); @@ -261,17 +253,13 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess catch (InvocationTargetException e) { throw new AssertionError(e.getMessage(), e); } - } - }, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method method) { - return method.getName().startsWith("standardClass"); - } - }); + }, + method -> method.getName().startsWith("standardClass") + ); } @Test - public void supportsXmlRootElement() throws Exception { + void supportsXmlRootElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class); marshaller.afterPropertiesSet(); @@ -284,7 +272,7 @@ public void supportsXmlRootElement() throws Exception { @Test - public void marshalAttachments() throws Exception { + void marshalAttachments() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(BinaryObject.class); marshaller.setMtomEnabled(true); @@ -304,7 +292,7 @@ public void marshalAttachments() throws Exception { } @Test // SPR-10714 - public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { + void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("org.springframework.oxm.jaxb"); marshaller.afterPropertiesSet(); @@ -318,7 +306,7 @@ public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Excepti } @Test // SPR-10806 - public void unmarshalStreamSourceWithXmlOptions() throws Exception { + void unmarshalStreamSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override @@ -352,7 +340,7 @@ public javax.xml.bind.Unmarshaller createUnmarshaller() { } @Test // SPR-10806 - public void unmarshalSaxSourceWithXmlOptions() throws Exception { + void unmarshalSaxSourceWithXmlOptions() throws Exception { final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class); Jaxb2Marshaller marshaller = new Jaxb2Marshaller() { @Override diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java index 0fd9e35fd586..4a4b9c9998ce 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2UnmarshallerTests.java @@ -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,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.oxm.AbstractUnmarshallerTests; import org.springframework.oxm.jaxb.test.FlightType; @@ -56,7 +57,7 @@ public class Jaxb2UnmarshallerTests extends AbstractUnmarshallerTests - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd b/spring-oxm/src/test/schema/flight.xsd similarity index 53% rename from spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd rename to spring-oxm/src/test/schema/flight.xsd index 5f46e0b91a0c..f27c3d5ee41d 100644 --- a/spring-oxm/src/test/resources/org/springframework/oxm/flight.xsd +++ b/spring-oxm/src/test/schema/flight.xsd @@ -1,4 +1,20 @@ + + diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 7dab1c8c21b9..232faade3c34 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 99a30e1cee11..fa52c987c667 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -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. @@ -373,8 +373,16 @@ private void params(MockHttpServletRequest request, UriComponents uriComponents) for (NameValuePair param : this.webRequest.getRequestParameters()) { if (param instanceof KeyDataPair) { KeyDataPair pair = (KeyDataPair) param; - MockPart part = new MockPart(pair.getName(), pair.getFile().getName(), readAllBytes(pair.getFile())); - part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + File file = pair.getFile(); + MockPart part; + if (file != null) { + part = new MockPart(pair.getName(), file.getName(), readAllBytes(file)); + part.getHeaders().setContentType(MediaType.valueOf(pair.getMimeType())); + } + else { // mimic empty file upload + part = new MockPart(pair.getName(), "", null); + part.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM); + } request.addPart(part); } else { diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 02e90ba16f6b..1b45d2d36c2a 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -496,7 +496,6 @@ void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; response.addHeader(SET_COOKIE, cookieValue); - System.err.println(response.getCookie("SESSION")); assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); assertNumCookies(1); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java index 27837936ad6c..a56fa8e91e65 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TimedTransactionalSpringExtensionTests.java @@ -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. @@ -67,7 +67,7 @@ void springTransactionsWorkWithJUnitJupiterTimeouts() { event(test("WithExceededJUnitJupiterTimeout"), finishedWithFailure( instanceOf(TimeoutException.class), - message(msg -> msg.endsWith("timed out after 50 milliseconds"))))); + message(msg -> msg.endsWith("timed out after 10 milliseconds"))))); } @@ -83,10 +83,10 @@ void transactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) void transactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isActive(); - Thread.sleep(100); + Thread.sleep(200); } @Test @@ -97,11 +97,11 @@ void notTransactionalWithJUnitJupiterTimeout() { } @Test - @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 10, unit = TimeUnit.MILLISECONDS) @Transactional(propagation = Propagation.NOT_SUPPORTED) void notTransactionalWithExceededJUnitJupiterTimeout() throws Exception { assertThatTransaction().isNotActive(); - Thread.sleep(100); + Thread.sleep(200); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java index 2daff9246a29..1a204d36166c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/TimedSpringRunnerTests.java @@ -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. @@ -76,14 +76,14 @@ public void springTimeoutWithNoOp() { } // Should Fail due to timeout. - @Test(timeout = 100) + @Test(timeout = 10) public void jUnitTimeoutWithSleep() throws Exception { Thread.sleep(200); } // Should Fail due to timeout. @Test - @Timed(millis = 100) + @Timed(millis = 10) public void springTimeoutWithSleep() throws Exception { Thread.sleep(200); } @@ -97,7 +97,7 @@ public void springTimeoutWithSleepAndMetaAnnotation() throws Exception { // Should Fail due to timeout. @Test - @MetaTimedWithOverride(millis = 100) + @MetaTimedWithOverride(millis = 10) public void springTimeoutWithSleepAndMetaAnnotationAndOverride() throws Exception { Thread.sleep(200); } @@ -110,7 +110,7 @@ public void springAndJUnitTimeouts() { } } - @Timed(millis = 100) + @Timed(millis = 10) @Retention(RetentionPolicy.RUNTIME) private static @interface MetaTimed { } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java index ad84f9ad890d..b1f73b4741f9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -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,8 +16,10 @@ package org.springframework.test.web.servlet.htmlunit; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -52,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -423,8 +426,7 @@ public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithMult } @Test // gh-24926 - public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() throws Exception { - + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithFileToUploadAsParameter() throws Exception { webRequest.setRequestParameters(Collections.singletonList( new KeyDataPair("key", new ClassPathResource("org/springframework/test/web/htmlunit/test.txt").getFile(), @@ -432,7 +434,7 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); - assertThat(actualRequest.getParts().size()).isEqualTo(1); + assertThat(actualRequest.getParts()).hasSize(1); Part part = actualRequest.getPart("key"); assertThat(part).isNotNull(); assertThat(part.getName()).isEqualTo("key"); @@ -441,6 +443,30 @@ public void buildRequestParameterMapViaWebRequestDotSetFileToUploadAsParameter() assertThat(part.getContentType()).isEqualTo(MimeType.TEXT_PLAIN); } + @Test // gh-26799 + public void buildRequestParameterMapViaWebRequestDotSetRequestParametersWithNullFileToUploadAsParameter() throws Exception { + webRequest.setRequestParameters(Collections.singletonList(new KeyDataPair("key", null, null, null, (Charset) null))); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParts()).hasSize(1); + Part part = actualRequest.getPart("key"); + + assertSoftly(softly -> { + softly.assertThat(part).isNotNull(); + softly.assertThat(part.getName()).as("name").isEqualTo("key"); + softly.assertThat(part.getSize()).as("size").isEqualTo(0); + try { + softly.assertThat(part.getInputStream()).isEmpty(); + } + catch (IOException ex) { + softly.fail("failed to get InputStream", ex); + } + softly.assertThat(part.getSubmittedFileName()).as("filename").isEqualTo(""); + softly.assertThat(part.getContentType()).as("content-type").isEqualTo("application/octet-stream"); + }); + } + @Test public void buildRequestParameterMapFromSingleQueryParam() throws Exception { webRequest.setUrl(new URL("https://example.com/example/?name=value")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index df9132d13d51..e1a403ebf97a 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -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,7 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NamedThreadLocal; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -320,7 +320,7 @@ public static List getSynchronizations() throws Ille else { // Sort lazily here, not in registerSynchronization. List sortedSynchs = new ArrayList<>(synchs); - AnnotationAwareOrderComparator.sort(sortedSynchs); + OrderComparator.sort(sortedSynchs); return Collections.unmodifiableList(sortedSynchs); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index b39b314c09b3..b1039145cf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -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. @@ -57,14 +57,13 @@ public static HttpMethod resolve(@Nullable String method) { /** - * Determine whether this {@code HttpMethod} matches the given - * method value. - * @param method the method value as a String + * Determine whether this {@code HttpMethod} matches the given method value. + * @param method the HTTP method as a String * @return {@code true} if it matches, {@code false} otherwise * @since 4.2.4 */ public boolean matches(String method) { - return (this == resolve(method)); + return name().equals(method); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index 215313900704..5e995f5007c1 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -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. @@ -416,6 +416,13 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, Series.SERVER_ERROR, "Network Authentication Required"); + private static final HttpStatus[] VALUES; + + static { + VALUES = values(); + } + + private final int value; private final Series series; @@ -550,7 +557,8 @@ public static HttpStatus valueOf(int statusCode) { */ @Nullable public static HttpStatus resolve(int statusCode) { - for (HttpStatus status : values()) { + // used cached VALUES instead of values() to prevent array allocation + for (HttpStatus status : VALUES) { if (status.value == statusCode) { return status; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java index 64c465035241..fcd2e3e7906c 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java @@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java new file mode 100644 index 000000000000..eb6b75b6b4ba --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java @@ -0,0 +1,128 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier scheduler; + + private volatile Mono directory = tempDirectory(); + + + public TempFileStorage(Supplier scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java index 3e684a47fb23..9de34009d480 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java @@ -578,9 +578,6 @@ public void createFile() { 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); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java index b914380f59a3..5cb374c77048 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java @@ -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,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux read(ResolvableType elementType, ReactiveHttpInputMessage message, Map hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index a432dc7a7809..0845a9f25f04 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -68,10 +68,10 @@ public abstract class AbstractListenerReadPublisher implements Publisher { @Nullable private volatile Subscriber super T> subscriber; - private volatile boolean completionBeforeDemand; + private volatile boolean completionPending; @Nullable - private volatile Throwable errorBeforeDemand; + private volatile Throwable errorPending; private final String logPrefix; @@ -186,7 +186,7 @@ public final void onError(Throwable ex) { */ private boolean readAndPublish() throws IOException { long r; - while ((r = this.demand) > 0 && !this.state.get().equals(State.COMPLETED)) { + while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); if (data != null) { if (r != Long.MAX_VALUE) { @@ -222,27 +222,30 @@ private void changeToDemandState(State oldState) { // Protect from infinite recursion in Undertow, where we can't check if data // is available, so all we can do is to try to read. // Generally, no need to check if we just came out of readAndPublish()... - if (!oldState.equals(State.READING)) { + if (oldState != State.READING) { checkOnDataAvailable(); } } } - private void handleCompletionOrErrorBeforeDemand() { + private boolean handlePendingCompletionOrError() { State state = this.state.get(); - if (!state.equals(State.UNSUBSCRIBED) && !state.equals(State.SUBSCRIBING)) { - if (this.completionBeforeDemand) { - rsReadLogger.trace(getLogPrefix() + "Completed before demand"); + if (state == State.DEMAND || state == State.NO_DEMAND) { + if (this.completionPending) { + rsReadLogger.trace(getLogPrefix() + "Processing pending completion"); this.state.get().onAllDataRead(this); + return true; } - Throwable ex = this.errorBeforeDemand; + Throwable ex = this.errorPending; if (ex != null) { if (rsReadLogger.isTraceEnabled()) { - rsReadLogger.trace(getLogPrefix() + "Completed with error before demand: " + ex); + rsReadLogger.trace(getLogPrefix() + "Processing pending completion with error: " + ex); } this.state.get().onError(this, ex); + return true; } } + return false; } private Subscription createSubscription() { @@ -305,7 +308,7 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); publisher.changeState(SUBSCRIBING, NO_DEMAND); - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.handlePendingCompletionOrError(); } else { throw new IllegalStateException("Failed to transition to SUBSCRIBING, " + @@ -315,14 +318,14 @@ void subscribe(AbstractListenerReadPublisher publisher, Subscriber supe @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -341,14 +344,14 @@ void request(AbstractListenerReadPublisher publisher, long n) { @Override void onAllDataRead(AbstractListenerReadPublisher publisher) { - publisher.completionBeforeDemand = true; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); } @Override void onError(AbstractListenerReadPublisher publisher, Throwable ex) { - publisher.errorBeforeDemand = ex; - publisher.handleCompletionOrErrorBeforeDemand(); + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); } }, @@ -379,14 +382,17 @@ void onDataAvailable(AbstractListenerReadPublisher publisher) { boolean demandAvailable = publisher.readAndPublish(); if (demandAvailable) { publisher.changeToDemandState(READING); + publisher.handlePendingCompletionOrError(); } else { publisher.readingPaused(); if (publisher.changeState(READING, NO_DEMAND)) { - // Demand may have arrived since readAndPublish returned - long r = publisher.demand; - if (r > 0) { - publisher.changeToDemandState(NO_DEMAND); + if (!publisher.handlePendingCompletionOrError()) { + // Demand may have arrived since readAndPublish returned + long r = publisher.demand; + if (r > 0) { + publisher.changeToDemandState(NO_DEMAND); + } } } } @@ -408,6 +414,18 @@ void request(AbstractListenerReadPublisher publisher, long n) { publisher.changeToDemandState(NO_DEMAND); } } + + @Override + void onAllDataRead(AbstractListenerReadPublisher publisher) { + publisher.completionPending = true; + publisher.handlePendingCompletionOrError(); + } + + @Override + void onError(AbstractListenerReadPublisher publisher, Throwable ex) { + publisher.errorPending = ex; + publisher.handlePendingCompletionOrError(); + } }, COMPLETED { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java index 10342d681d10..1d04470065b1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteFlushProcessor.java @@ -329,7 +329,7 @@ public void writeComplete(AbstractListenerWriteFlushProcessor processor) public void onComplete(AbstractListenerWriteFlushProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { handleSourceCompleted(processor); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index 6cfd8412a622..92d7b41846b5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -151,10 +151,11 @@ public final void onComplete() { * container. */ public final void onWritePossible() { + State state = this.state.get(); if (rsWriteLogger.isTraceEnabled()) { - rsWriteLogger.trace(getLogPrefix() + "onWritePossible"); + rsWriteLogger.trace(getLogPrefix() + "onWritePossible [" + state + "]"); } - this.state.get().onWritePossible(this); + state.onWritePossible(this); } /** @@ -182,14 +183,14 @@ void cancelAndSetCompleted() { cancel(); for (;;) { State prev = this.state.get(); - if (prev.equals(State.COMPLETED)) { + if (prev == State.COMPLETED) { break; } if (this.state.compareAndSet(prev, State.COMPLETED)) { if (rsWriteLogger.isTraceEnabled()) { rsWriteLogger.trace(getLogPrefix() + prev + " -> " + this.state); } - if (!prev.equals(State.WRITING)) { + if (prev != State.WRITING) { discardCurrentData(); } break; @@ -429,7 +430,7 @@ else if (processor.changeState(this, WRITING)) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } @@ -440,7 +441,7 @@ public void onComplete(AbstractListenerWriteProcessor processor) { public void onComplete(AbstractListenerWriteProcessor processor) { processor.sourceCompleted = true; // A competing write might have completed very quickly - if (processor.state.get().equals(State.REQUESTED)) { + if (processor.state.get() == State.REQUESTED) { processor.changeStateToComplete(State.REQUESTED); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index b705df0da388..c38837c7ed03 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -157,7 +157,7 @@ private String getServletPath(ServletConfig config) { @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { // Check for existing error attribute first - if (DispatcherType.ASYNC.equals(request.getDispatcherType())) { + if (DispatcherType.ASYNC == request.getDispatcherType()) { Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME); throw new ServletException("Failed to create response content", ex); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 9bac8734bc56..63ac63dd3557 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -182,14 +182,14 @@ void subscribe(WriteResultPublisher publisher, Subscriber super Void> subscrib @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } @@ -203,14 +203,14 @@ void request(WriteResultPublisher publisher, long n) { @Override void publishComplete(WriteResultPublisher publisher) { publisher.completedBeforeSubscribed = true; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishComplete(publisher); } } @Override void publishError(WriteResultPublisher publisher, Throwable ex) { publisher.errorBeforeSubscribed = ex; - if(State.SUBSCRIBED.equals(publisher.state.get())) { + if(State.SUBSCRIBED == publisher.state.get()) { publisher.state.get().publishError(publisher, ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 99b6627b5e2c..ed7855e79097 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -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,7 +34,7 @@ /** * Specialized {@link org.springframework.validation.DataBinder} to perform data - * binding from URL query params or form data in the request data to Java objects. + * binding from URL query parameters or form data in the request data to Java objects. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -64,7 +64,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) { /** - * Bind query params, form data, and or multipart form data to the binder target. + * Bind query parameters, form data, or multipart form data to the binder target. * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ diff --git a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java index b319a3d8c6a2..ab2a0f6042c7 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/SpringBeanAutowiringSupport.java @@ -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. @@ -85,10 +85,11 @@ public static void processInjectionBasedOnCurrentContext(Object target) { bpp.processInjection(target); } else { - if (logger.isDebugEnabled()) { - logger.debug("Current WebApplicationContext is not available for processing of " + + if (logger.isWarnEnabled()) { + logger.warn("Current WebApplicationContext is not available for processing of " + ClassUtils.getShortName(target.getClass()) + ": " + - "Make sure this class gets constructed in a Spring web application. Proceeding without injection."); + "Make sure this class gets constructed in a Spring web application after the" + + "Spring WebApplicationContext has been initialized. Proceeding without injection."); } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6c0591d6d20b..1eee79898c10 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -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. @@ -138,7 +138,12 @@ public CorsConfiguration(CorsConfiguration other) { * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. */ public void setAllowedOrigins(@Nullable List allowedOrigins) { - this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null); + this.allowedOrigins = (allowedOrigins != null ? + allowedOrigins.stream().map(this::trimTrailingSlash).collect(Collectors.toList()) : null); + } + + private String trimTrailingSlash(String origin) { + return origin.endsWith("/") ? origin.substring(0, origin.length() - 1) : origin; } /** @@ -159,6 +164,7 @@ public void addAllowedOrigin(String origin) { else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { setAllowedOrigins(DEFAULT_PERMIT_ALL); } + origin = trimTrailingSlash(origin); this.allowedOrigins.add(origin); } @@ -209,6 +215,7 @@ public void addAllowedOriginPattern(String originPattern) { if (this.allowedOriginPatterns == null) { this.allowedOriginPatterns = new ArrayList<>(4); } + originPattern = trimTrailingSlash(originPattern); this.allowedOriginPatterns.add(new OriginPattern(originPattern)); if (this.allowedOrigins == DEFAULT_PERMIT_ALL) { this.allowedOrigins = null; @@ -475,7 +482,6 @@ public void validateAllowCredentials() { * @return the combined {@code CorsConfiguration}, or {@code this} * configuration if the supplied configuration is {@code null} */ - @Nullable public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (other == null) { return this; @@ -543,30 +549,31 @@ private List combinePatterns( /** * Check the origin of the request against the configured allowed origins. - * @param requestOrigin the origin to check + * @param origin the origin to check * @return the origin to use for the response, or {@code null} which * means the request origin is not allowed */ @Nullable - public String checkOrigin(@Nullable String requestOrigin) { - if (!StringUtils.hasText(requestOrigin)) { + public String checkOrigin(@Nullable String origin) { + if (!StringUtils.hasText(origin)) { return null; } + String originToCheck = trimTrailingSlash(origin); if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { - if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { - return requestOrigin; + if (originToCheck.equalsIgnoreCase(allowedOrigin)) { + return origin; } } } if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { for (OriginPattern p : this.allowedOriginPatterns) { - if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(requestOrigin).matches()) { - return requestOrigin; + if (p.getDeclaredPattern().equals(ALL) || p.getPattern().matcher(originToCheck).matches()) { + return origin; } } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java index 768cb78ca990..498199e283a9 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestHandler.java @@ -25,6 +25,7 @@ * * @author Rossen Stoyanchev * @since 5.3.4 + * @see PreFlightRequestWebFilter */ public interface PreFlightRequestHandler { diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java new file mode 100644 index 000000000000..1b9f6adf42bd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/PreFlightRequestWebFilter.java @@ -0,0 +1,57 @@ +/* + * 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.cors.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * WebFilter that handles pre-flight requests through a + * {@link PreFlightRequestHandler} and bypasses the rest of the chain. + * + * A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
A WebFlux application can simply inject PreFlightRequestHandler and use + * it to create an instance of this WebFilter since {@code @EnableWebFlux} + * declares {@code DispatcherHandler} as a bean and that is a + * PreFlightRequestHandler. + * + * @author Rossen Stoyanchev + * @since 5.3.7 + */ +public class PreFlightRequestWebFilter implements WebFilter { + + private final PreFlightRequestHandler handler; + + + /** + * Create an instance that will delegate to the given handler. + */ + public PreFlightRequestWebFilter(PreFlightRequestHandler handler) { + Assert.notNull(handler, "PreFlightRequestHandler is required"); + this.handler = handler; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return (CorsUtils.isPreFlightRequest(exchange.getRequest()) ? + this.handler.handlePreFlight(exchange) : chain.filter(exchange)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index c09d9ec75348..cd63b46290dd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -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.web.method.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; @@ -37,16 +38,16 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -76,6 +77,7 @@ * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Vladislav Kisel * @since 3.1 */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @@ -256,6 +258,14 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M String paramName = paramNames[i]; Class> paramType = paramTypes[i]; Object value = webRequest.getParameterValues(paramName); + + // Since WebRequest#getParameter exposes a single-value parameter as an array + // with a single element, we unwrap the single value in such cases, analogous + // to WebExchangeDataBinder.addBindValue(Map, String, List>). + if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { + value = Array.get(value, 0); + } + if (value == null) { if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); @@ -269,6 +279,7 @@ protected Object constructAttribute(Constructor> ctor, String attributeName, M } } } + try { MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName); if (value == null && methodParam.isOptional()) { @@ -362,7 +373,7 @@ else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "mu */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; @@ -388,7 +399,7 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p Class> targetType, String fieldName, @Nullable Object value) { for (Annotation ann : parameter.getParameterAnnotations()) { - Object[] validationHints = determineValidationHints(ann); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { for (Validator validator : binder.getValidators()) { if (validator instanceof SmartValidator) { @@ -406,26 +417,6 @@ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter p } } - /** - * Determine any validation triggered by the given annotation. - * @param ann the annotation (potentially a validation annotation) - * @return the validation hints to apply (possibly an empty array), - * or {@code null} if this annotation does not trigger any validation - * @since 5.1 - */ - @Nullable - private Object[] determineValidationHints(Annotation ann) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints == null) { - return new Object[0]; - } - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - } - return null; - } - /** * Whether to raise a fatal bind exception on validation errors. * The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}. diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index ebe9d5133e5c..7779aff4afeb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -85,7 +85,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; + private static final String PORT_PATTERN = "(.[^/?#]*(?:\\{[^/]+?})?)"; private static final String PATH_PATTERN = "([^?#]*)"; diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java new file mode 100644 index 000000000000..223465ce3dac --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java @@ -0,0 +1,82 @@ +/* + * 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.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java index e929dcb67c5e..7649e8415bd5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java @@ -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. @@ -72,7 +72,7 @@ public void canReadAndWriteMicroformats() { public void readTyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); MyBean result = (MyBean) this.converter.read(MyBean.class, inputMessage); @@ -90,7 +90,7 @@ public void readTyped() throws IOException { public void readUntyped() throws IOException { String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage); assertThat(result.get("string")).isEqualTo("Foo"); @@ -167,9 +167,9 @@ public void writeUTF16() throws IOException { } @Test - public void readInvalidJson() throws IOException { + public void readInvalidJson() { String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> this.converter.read(MyBean.class, inputMessage)); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java index 96539ca8f150..d54f09f09d52 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderIntegrationTests.java @@ -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. @@ -32,10 +32,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,23 +49,22 @@ * @author Brian Clozel * @author Sam Brannen */ -public class WebRequestDataBinderIntegrationTests { +@TestInstance(Lifecycle.PER_CLASS) +class WebRequestDataBinderIntegrationTests { - private static Server jettyServer; + private final PartsServlet partsServlet = new PartsServlet(); - private static final PartsServlet partsServlet = new PartsServlet(); - - private static final PartListServlet partListServlet = new PartListServlet(); + private final PartListServlet partListServlet = new PartListServlet(); private final RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); - protected static String baseUrl; + private Server jettyServer; - protected static MediaType contentType; + private String baseUrl; @BeforeAll - public static void startJettyServer() throws Exception { + void startJettyServer() throws Exception { // Let server pick its own random, available port. jettyServer = new Server(0); @@ -89,7 +89,7 @@ public static void startJettyServer() throws Exception { } @AfterAll - public static void stopJettyServer() throws Exception { + void stopJettyServer() throws Exception { if (jettyServer != null) { jettyServer.stop(); } @@ -97,7 +97,7 @@ public static void stopJettyServer() throws Exception { @Test - public void partsBinding() { + void partsBinding() { PartsBean bean = new PartsBean(); partsServlet.setBean(bean); @@ -113,7 +113,7 @@ public void partsBinding() { } @Test - public void partListBinding() { + void partListBinding() { PartListBean bean = new PartListBean(); partListServlet.setBean(bean); @@ -143,7 +143,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_OK); } - public void setBean(T bean) { + void setBean(T bean) { this.bean = bean; } } @@ -151,9 +151,9 @@ public void setBean(T bean) { private static class PartsBean { - public Part firstPart; + private Part firstPart; - public Part secondPart; + private Part secondPart; public Part getFirstPart() { return firstPart; @@ -182,7 +182,7 @@ private static class PartsServlet extends AbstractStandardMultipartServlet partList; + private List partList; public List getPartList() { return partList; diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 82c5286dce7b..b920a9f16792 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -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. @@ -282,15 +282,24 @@ public void combine() { @Test public void checkOriginAllowed() { + // "*" matches CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("*"); + // "*" does not match together with allowCredentials config.setAllowCredentials(true); assertThatIllegalArgumentException().isThrownBy(() -> config.checkOrigin("https://domain.com")); + // specific origin matches Origin header with or without trailing "/" config.setAllowedOrigins(Collections.singletonList("https://domain.com")); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); + + // specific origin with trailing "/" matches Origin header with or without trailing "/" + config.setAllowedOrigins(Collections.singletonList("https://domain.com/")); + assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); + assertThat(config.checkOrigin("https://domain.com/")).isEqualTo("https://domain.com/"); config.setAllowCredentials(false); assertThat(config.checkOrigin("https://domain.com")).isEqualTo("https://domain.com"); diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 5c163779723c..c57aeffeadab 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -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. @@ -170,10 +170,19 @@ public void actualRequestCaseInsensitiveOriginMatch() throws Exception { this.conf.addAllowedOrigin("https://DOMAIN2.com"); this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, - HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() throws Exception { + this.request.setMethod(HttpMethod.GET.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com/"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index 4549d1409a74..36b5a4787e95 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -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. @@ -172,10 +172,22 @@ public void actualRequestCaseInsensitiveOriginMatch() { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); + assertThat((Object) response.getStatusCode()).isNull(); assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, - ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + } + + @Test // gh-26892 + public void actualRequestTrailingSlashOriginMatch() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .method(HttpMethod.GET, "http://localhost/test.html") + .header(HttpHeaders.ORIGIN, "https://domain2.com/")); + + this.conf.addAllowedOrigin("https://domain2.com"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); assertThat((Object) response.getStatusCode()).isNull(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 038f28bfa347..bc3be0e7aa99 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -58,6 +60,7 @@ * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev + * @author Vladislav Kisel */ public class ModelAttributeMethodProcessorTests { @@ -73,6 +76,7 @@ public class ModelAttributeMethodProcessorTests { private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; + private MethodParameter beanWithConstructorArgs; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @@ -86,7 +90,7 @@ public void setup() throws Exception { Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, - TestBean.class, TestBean.class); + TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); @@ -94,6 +98,7 @@ public void setup() throws Exception { this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); + this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); @@ -264,6 +269,26 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } + @Test // gh-25182 + public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addParameter("listOfStrings", "1,2"); + ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); + + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) + .willAnswer(invocation -> { + WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); + + // Add conversion service which will convert "1,2" to a list + binder.setConversionService(new DefaultFormattingConversionService()); + return binder; + }); + + Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory); + assertThat(resolved).isInstanceOf(TestBeanWithConstructorArgs.class); + assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).containsExactly("1", "2"); + } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); @@ -330,10 +355,20 @@ public void modelAttribute( int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, - TestBean notAnnotatedAttr) { + TestBean notAnnotatedAttr, + TestBeanWithConstructorArgs beanWithConstructorArgs) { } } + static class TestBeanWithConstructorArgs { + + final List listOfStrings; + + public TestBeanWithConstructorArgs(List listOfStrings) { + this.listOfStrings = listOfStrings; + } + + } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index 1db9b40628c5..2da0fc9b2857 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link UriComponentsBuilder}. @@ -1272,4 +1273,28 @@ void verifyDoubleSlashReplacedWithSingleOne() { assertThat(path).isEqualTo("/home/path"); } + @Test + void validPort() { + UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567/path").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getPath()).isEqualTo("/path"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567?trace=false").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getQuery()).isEqualTo("trace=false"); + + uriComponents = UriComponentsBuilder.fromUriString("http://localhost:52567#fragment").build(); + assertThat(uriComponents.getPort()).isEqualTo(52567); + assertThat(uriComponents.getFragment()).isEqualTo("fragment"); + } + + @Test + void verifyInvalidPort() { + String url = "http://localhost:port/path"; + assertThatThrownBy(() -> UriComponentsBuilder.fromUriString(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + assertThatThrownBy(() -> UriComponentsBuilder.fromHttpUrl(url).build().toUri()) + .isInstanceOf(NumberFormatException.class); + } + } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java index b6140042e0cb..978bdf09b053 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockServletContext.java @@ -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. @@ -315,8 +315,8 @@ public Set getResourcePaths(String path) { return resourcePaths; } catch (InvalidPathException | IOException ex ) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get resource paths for " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException { throw ex; } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not get URL for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) { return resource.getInputStream(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not open InputStream for resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + (resource != null ? resource : resourceLocation), ex); } return null; @@ -476,8 +476,8 @@ public String getRealPath(String path) { return resource.getFile().getAbsolutePath(); } catch (InvalidPathException | IOException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Could not determine real path of resource " + + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + (resource != null ? resource : resourceLocation), ex); } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index ce7aa0130329..327c83ff8177 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -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. @@ -35,7 +35,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -46,10 +46,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + *
By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -57,9 +61,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * @link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. *
By default this is not set. * @since 5.3 */ @@ -143,7 +149,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 6d0331b9bd49..927fcdf205d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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,7 +17,6 @@ package org.springframework.web.reactive.function.client; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -207,9 +206,7 @@ public Mono createException() { .onErrorReturn(IllegalStateException.class::isInstance, EMPTY) .map(bodyBytes -> { HttpRequest request = this.requestSupplier.get(); - Charset charset = headers().contentType() - .map(MimeType::getCharset) - .orElse(StandardCharsets.ISO_8859_1); + Charset charset = headers().contentType().map(MimeType::getCharset).orElse(null); int statusCode = rawStatusCode(); HttpStatus httpStatus = HttpStatus.resolve(statusCode); if (httpStatus != null) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java index 12fb186a539f..d11bc4eabca9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunction.java @@ -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. @@ -39,6 +39,13 @@ public interface ExchangeFilterFunction { * in the chain, to be invoked via * {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order to * proceed with the exchange, or not invoked to shortcut the chain. + * + * Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + * Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
Note: When a filter handles the response after the + * call to {@link ExchangeFunction#exchange}, extra care must be taken to + * always consume its content or otherwise propagate it downstream for + * further handling, for example by the {@link WebClient}. Please, see the + * reference documentation for more details on this. + * * @param request the current request * @param next the next exchange function in the chain * @return the filtered response diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java index 79fe6f708cdd..6d35b6594cc5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunction.java @@ -43,6 +43,14 @@ public interface ExchangeFunction { /** * Exchange the given request for a {@link ClientResponse} promise. + * + *
Note: When calling this method from an + * {@link ExchangeFilterFunction} that handles the response in some way, + * extra care must be taken to always consume its content or otherwise + * propagate it downstream for further handling, for example by the + * {@link WebClient}. Please, see the reference documentation for more + * details on this. + * * @param request the request to exchange * @return the delayed response */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java index 50c53a52f683..07550a11dbd2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/UnknownHttpStatusCodeException.java @@ -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. @@ -50,7 +50,7 @@ public UnknownHttpStatusCodeException( * @since 5.1.4 */ public UnknownHttpStatusCodeException( - int statusCode, HttpHeaders headers, byte[] responseBody, Charset responseCharset, + int statusCode, HttpHeaders headers, byte[] responseBody, @Nullable Charset responseCharset, @Nullable HttpRequest request) { super("Unknown status code [" + statusCode + "]", statusCode, "", diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c43566e6319f..801609d68fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. @@ -186,13 +186,6 @@ interface Builder { */ Builder baseUrl(String baseUrl); - /** - * Configure default URI variable values that will be used when expanding - * URI templates using a {@link Map}. - * @param defaultUriVariables the default values to use - * @see #baseUrl(String) - * @see #uriBuilderFactory(UriBuilderFactory) - */ /** * Configure default URL variable values to use when expanding URI * templates with a {@link Map}. Effectively a shortcut for: diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 82d246c3f009..ab211917b5f4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -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. @@ -43,6 +43,7 @@ public class WebClientResponseException extends WebClientException { private final HttpHeaders headers; + @Nullable private final Charset responseCharset; @Nullable @@ -97,7 +98,7 @@ public WebClientResponseException(String message, int statusCode, String statusT this.statusText = statusText; this.headers = (headers != null ? headers : HttpHeaders.EMPTY); this.responseBody = (responseBody != null ? responseBody : new byte[0]); - this.responseCharset = (charset != null ? charset : StandardCharsets.ISO_8859_1); + this.responseCharset = charset; this.request = request; } @@ -139,10 +140,26 @@ public byte[] getResponseBodyAsByteArray() { } /** - * Return the response body as a string. + * Return the response content as a String using the charset of media type + * for the response, if available, or otherwise falling back on + * {@literal ISO-8859-1}. Use {@link #getResponseBodyAsString(Charset)} if + * you want to fall back on a different, default charset. */ public String getResponseBodyAsString() { - return new String(this.responseBody, this.responseCharset); + return getResponseBodyAsString(StandardCharsets.ISO_8859_1); + } + + /** + * Variant of {@link #getResponseBodyAsString()} that allows specifying the + * charset to fall back on, if a charset is not available from the media + * type for the response. + * @param defaultCharset the charset to use if the {@literal Content-Type} + * of the response does not specify one. + * @since 5.3.7 + */ + public String getResponseBodyAsString(Charset defaultCharset) { + return new String(this.responseBody, + (this.responseCharset != null ? this.responseCharset : defaultCharset)); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index c278ca059711..07a7e70f4861 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -31,7 +31,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; import org.springframework.core.codec.Hints; import org.springframework.core.io.buffer.DataBuffer; @@ -45,7 +44,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; @@ -240,10 +239,9 @@ private ServerWebInputException handleMissingBody(MethodParameter parameter) { private Object[] extractValidationHints(MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] hints = ValidationAnnotationUtils.determineValidationHints(ann); + if (hints != null) { + return hints; } } return null; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 645ae8e19e41..230ed80958aa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -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. @@ -30,14 +30,13 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -61,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen * @since 5.0 */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -118,7 +118,7 @@ public Mono resolveArgument( return valueMono.flatMap(value -> { WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); - return bindRequestParameters(binder, exchange) + return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange)) .doOnError(bindingResultSink::tryEmitError) .doOnSuccess(aVoid -> { validateIfApplicable(binder, parameter); @@ -144,6 +144,16 @@ public Mono resolveArgument( }); } + /** + * Determine if binding should be disabled for the supplied {@link MethodParameter}, + * based on the {@link ModelAttribute#binding} annotation attribute. + * @since 5.2.15 + */ + private boolean bindingDisabled(MethodParameter parameter) { + ModelAttribute modelAttribute = parameter.getParameterAnnotation(ModelAttribute.class); + return (modelAttribute != null && !modelAttribute.binding()); + } + /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -270,16 +280,9 @@ private boolean hasErrorsArgument(MethodParameter parameter) { private void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) { for (Annotation ann : parameter.getParameterAnnotations()) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - if (hints != null) { - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - binder.validate(validationHints); - } - else { - binder.validate(); - } + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + binder.validate(validationHints); } } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 6974faee6d6b..f04000ce46d9 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -531,8 +531,8 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct fun filter(filterFunction: suspend (ServerRequest, suspend (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { serverRequest, handlerFunction -> mono(Dispatchers.Unconfined) { - filterFunction(serverRequest) { - handlerFunction.handle(serverRequest).awaitSingle() + filterFunction(serverRequest) { handlerRequest -> + handlerFunction.handle(handlerRequest).awaitSingle() } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index b4dc68898ff8..a3f632a5e6ec 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -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. @@ -73,4 +73,24 @@ public void allowCredentials() { .containsExactly("*"); } + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index cb8052d751dd..514dd48d955f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Function; +import javax.validation.constraints.NotEmpty; + import io.reactivex.rxjava3.core.Single; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,16 +51,17 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ -public class ModelAttributeMethodArgumentResolverTests { +class ModelAttributeMethodArgumentResolverTests { - private BindingContext bindContext; + private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); - private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); + private BindingContext bindContext; @BeforeEach - public void setup() throws Exception { + void setup() { LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); @@ -68,32 +71,38 @@ public void setup() throws Exception { @Test - public void supports() throws Exception { + void supports() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false); - MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + assertThat(resolver.supportsParameter(param)).isTrue(); + + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isFalse(); } @Test - public void supportsWithDefaultResolution() throws Exception { + void supportsWithDefaultResolution() { ModelAttributeMethodArgumentResolver resolver = new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), true); - MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + MethodParameter param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); - param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); assertThat(resolver.supportsParameter(param)).isTrue(); param = this.testMethod.annotNotPresent(ModelAttribute.class).arg(String.class); @@ -104,204 +113,286 @@ public void supportsWithDefaultResolution() throws Exception { } @Test - public void createAndBind() throws Exception { - testBindFoo("foo", this.testMethod.annotPresent(ModelAttribute.class).arg(Foo.class), value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createAndBind() throws Exception { + testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToMono() throws Exception { + void createAndBindToMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo("fooMono", parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo("pojoMono", parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void createAndBindToSingle() throws Exception { + void createAndBindToSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); - testBindFoo("fooSingle", parameter, single -> { - boolean condition = single instanceof Single; - assertThat(condition).as(single.getClass().getName()).isTrue(); + testBindPojo("pojoSingle", parameter, single -> { + assertThat(single).isInstanceOf(Single.class); Object value = ((Single>) single).blockingGet(); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } @Test - public void bindExisting() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute(foo); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBind() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojo", parameter, value -> { + assertThat(value).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) value; }); + } - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + @Test + void createButDoNotBindToMono() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoMono", parameter, value -> { + assertThat(value).isInstanceOf(Mono.class); + Object extractedValue = ((Mono>) value).block(Duration.ofSeconds(5)); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; + }); } @Test - public void bindExistingMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooMono", Mono.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void createButDoNotBindToSingle() throws Exception { + MethodParameter parameter = + this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class); + + createButDoNotBindToPojo("nonBindingPojoSingle", parameter, value -> { + assertThat(value).isInstanceOf(Single.class); + Object extractedValue = ((Single>) value).blockingGet(); + assertThat(extractedValue).isInstanceOf(NonBindingPojo.class); + return (NonBindingPojo) extractedValue; }); + } + + private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter, + Function valueExtractor) throws Exception { + + Object value = createResolver() + .resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma")) + .block(Duration.ZERO); + + NonBindingPojo nonBindingPojo = valueExtractor.apply(value); + assertThat(nonBindingPojo).isNotNull(); + assertThat(nonBindingPojo.getName()).isNull(); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; + + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(nonBindingPojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void bindExistingSingle() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - this.bindContext.getModel().addAttribute("fooSingle", Single.just(foo)); - - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); - testBindFoo("foo", parameter, value -> { - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + void bindExisting() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute(pojo); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); - assertThat(this.bindContext.getModel().asMap().get("foo")).isSameAs(foo); + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); } @Test - public void bindExistingMonoToMono() throws Exception { - Foo foo = new Foo(); - foo.setName("Jim"); - String modelKey = "fooMono"; - this.bindContext.getModel().addAttribute(modelKey, Mono.just(foo)); + void bindExistingMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingSingle() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo)); + + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); + testBindPojo("pojo", parameter, value -> { + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; + }); + + assertThat(this.bindContext.getModel().asMap().get("pojo")).isSameAs(pojo); + } + + @Test + void bindExistingMonoToMono() throws Exception { + Pojo pojo = new Pojo(); + pojo.setName("Jim"); + String modelKey = "pojoMono"; + this.bindContext.getModel().addAttribute(modelKey, Mono.just(pojo)); MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); - testBindFoo(modelKey, parameter, mono -> { - boolean condition = mono instanceof Mono; - assertThat(condition).as(mono.getClass().getName()).isTrue(); + testBindPojo(modelKey, parameter, mono -> { + assertThat(mono).isInstanceOf(Mono.class); Object value = ((Mono>) mono).block(Duration.ofSeconds(5)); - assertThat(value.getClass()).isEqualTo(Foo.class); - return (Foo) value; + assertThat(value.getClass()).isEqualTo(Pojo.class); + return (Pojo) value; }); } - private void testBindFoo(String modelKey, MethodParameter param, Function valueExtractor) + private void testBindPojo(String modelKey, MethodParameter param, Function valueExtractor) throws Exception { Object value = createResolver() .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25")) .block(Duration.ZERO); - Foo foo = valueExtractor.apply(value); - assertThat(foo.getName()).isEqualTo("Robert"); - assertThat(foo.getAge()).isEqualTo(25); + Pojo pojo = valueExtractor.apply(value); + assertThat(pojo.getName()).isEqualTo("Robert"); + assertThat(pojo.getAge()).isEqualTo(25); String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(modelKey)).isSameAs(foo); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(pojo); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } @Test - public void validationError() throws Exception { - MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Foo.class); + void validationErrorForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class); testValidationError(parameter, Function.identity()); } @Test - public void validationErrorToMono() throws Exception { + void validationErrorForMono() throws Exception { MethodParameter parameter = this.testMethod - .annotNotPresent(ModelAttribute.class).arg(Mono.class, Foo.class); + .annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Mono; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Mono.class); return (Mono>) value; }); } @Test - public void validationErrorToSingle() throws Exception { + void validationErrorForSingle() throws Exception { MethodParameter parameter = this.testMethod - .annotPresent(ModelAttribute.class).arg(Single.class, Foo.class); + .annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class); testValidationError(parameter, resolvedArgumentMono -> { Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); - assertThat(value).isNotNull(); - boolean condition = value instanceof Single; - assertThat(condition).isTrue(); + assertThat(value).isInstanceOf(Single.class); return Mono.from(((Single>) value).toFlowable()); }); } - private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor) + @Test + void validationErrorWithoutBindingForPojo() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class); + testValidationErrorWithoutBinding(parameter, Function.identity()); + } + + @Test + void validationErrorWithoutBindingForMono() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Mono.class); + return (Mono>) value; + }); + } + + @Test + void validationErrorWithoutBindingForSingle() throws Exception { + MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, ValidatedPojo.class); + + testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> { + Object value = resolvedArgumentMono.block(Duration.ofSeconds(5)); + assertThat(value).isInstanceOf(Single.class); + return Mono.from(((Single>) value).toFlowable()); + }); + } + + private void testValidationError(MethodParameter parameter, Function, Mono>> valueMonoExtractor) + throws URISyntaxException { + + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); + } + + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono>> valueMonoExtractor) throws URISyntaxException { - ServerWebExchange exchange = postForm("age=invalid"); - Mono> mono = createResolver().resolveArgument(param, this.bindContext, exchange); + testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); + } + + private void testValidationError(MethodParameter param, Function, Mono>> valueMonoExtractor, + String formData, String field, String rejectedValue) throws URISyntaxException { + + Mono> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData)); mono = valueMonoExtractor.apply(mono); StepVerifier.create(mono) .consumeErrorWith(ex -> { - boolean condition = ex instanceof WebExchangeBindException; - assertThat(condition).isTrue(); + assertThat(ex).isInstanceOf(WebExchangeBindException.class); WebExchangeBindException bindException = (WebExchangeBindException) ex; assertThat(bindException.getErrorCount()).isEqualTo(1); - assertThat(bindException.hasFieldErrors("age")).isTrue(); + assertThat(bindException.hasFieldErrors(field)).isTrue(); + assertThat(bindException.getFieldError(field).getRejectedValue()).isEqualTo(rejectedValue); }) .verify(); } @Test - public void bindDataClass() throws Exception { - testBindBar(this.testMethod.annotNotPresent(ModelAttribute.class).arg(Bar.class)); - } + void bindDataClass() throws Exception { + MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class); - private void testBindBar(MethodParameter param) throws Exception { Object value = createResolver() - .resolveArgument(param, this.bindContext, postForm("name=Robert&age=25&count=1")) + .resolveArgument(parameter, this.bindContext, postForm("name=Robert&age=25&count=1")) .block(Duration.ZERO); - Bar bar = (Bar) value; - assertThat(bar.getName()).isEqualTo("Robert"); - assertThat(bar.getAge()).isEqualTo(25); - assertThat(bar.getCount()).isEqualTo(1); + DataClass dataClass = (DataClass) value; + assertThat(dataClass.getName()).isEqualTo("Robert"); + assertThat(dataClass.getAge()).isEqualTo(25); + assertThat(dataClass.getCount()).isEqualTo(1); - String key = "bar"; - String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key; + String modelKey = "dataClass"; + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + modelKey; - Map map = bindContext.getModel().asMap(); - assertThat(map.size()).as(map.toString()).isEqualTo(2); - assertThat(map.get(key)).isSameAs(bar); - assertThat(map.get(bindingResultKey)).isNotNull(); - boolean condition = map.get(bindingResultKey) instanceof BindingResult; - assertThat(condition).isTrue(); + Map model = bindContext.getModel().asMap(); + assertThat(model).hasSize(2); + assertThat(model.get(modelKey)).isSameAs(dataClass); + assertThat(model.get(bindingResultKey)).isInstanceOf(BindingResult.class); } // TODO: SPR-15871, SPR-15542 @@ -320,31 +411,30 @@ private ServerWebExchange postForm(String formData) throws URISyntaxException { @SuppressWarnings("unused") void handle( - @ModelAttribute @Validated Foo foo, - @ModelAttribute @Validated Mono mono, - @ModelAttribute @Validated Single single, - Foo fooNotAnnotated, + @ModelAttribute @Validated Pojo pojo, + @ModelAttribute @Validated Mono mono, + @ModelAttribute @Validated Single single, + @ModelAttribute(binding = false) NonBindingPojo nonBindingPojo, + @ModelAttribute(binding = false) Mono monoNonBindingPojo, + @ModelAttribute(binding = false) Single singleNonBindingPojo, + @ModelAttribute(binding = false) @Validated ValidatedPojo validatedPojo, + @ModelAttribute(binding = false) @Validated Mono monoValidatedPojo, + @ModelAttribute(binding = false) @Validated Single singleValidatedPojo, + Pojo pojoNotAnnotated, String stringNotAnnotated, - Mono monoNotAnnotated, + Mono monoNotAnnotated, Mono monoStringNotAnnotated, - Bar barNotAnnotated) { + DataClass dataClassNotAnnotated) { } @SuppressWarnings("unused") - private static class Foo { + private static class Pojo { private String name; private int age; - public Foo() { - } - - public Foo(String name) { - this.name = name; - } - public String getName() { return name; } @@ -364,7 +454,48 @@ public void setAge(int age) { @SuppressWarnings("unused") - private static class Bar { + private static class NonBindingPojo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "NonBindingPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class ValidatedPojo { + + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ValidatedPojo [name=" + name + "]"; + } + } + + + @SuppressWarnings("unused") + private static class DataClass { private final String name; @@ -372,7 +503,7 @@ private static class Bar { private int count; - public Bar(String name, int age) { + public DataClass(String name, int age) { this.name = name; this.age = age; } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 1a2bc064463c..bdeae8b00af7 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -152,6 +152,16 @@ class CoRouterFunctionDslTests { } } + @Test + fun filtering() { + val mockRequest = get("https://example.com/filter").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { response -> + response.headers().getFirst("foo") == "bar" + } + .verifyComplete() + } private fun sampleRouter() = coRouter { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } @@ -186,6 +196,18 @@ class CoRouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = coRouter { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).buildAndAwait() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 394780c95d5f..1486837d7f92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -49,6 +49,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.HttpMethod; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.lang.Nullable; @@ -968,7 +969,9 @@ protected void doService(HttpServletRequest request, HttpServletResponse respons restoreAttributesAfterInclude(request, attributesSnapshot); } } - ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } } } @@ -1044,8 +1047,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c8cddf01e42a..6d3e8d3d2b45 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1085,7 +1085,7 @@ private void logResult(HttpServletRequest request, HttpServletResponse response, } DispatcherType dispatchType = request.getDispatcherType(); - boolean initialDispatch = DispatcherType.REQUEST.equals(request.getDispatcherType()); + boolean initialDispatch = DispatcherType.REQUEST == dispatchType; if (failureCause != null) { if (!initialDispatch) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index f60ff3770a0a..523f5dcc0c5c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -36,7 +36,7 @@ public class CorsRegistration { private final String pathPattern; - private final CorsConfiguration config; + private CorsConfiguration config; public CorsRegistration(String pathPattern) { @@ -47,10 +47,14 @@ public CorsRegistration(String pathPattern) { /** - * A list of origins for which cross-origin requests are allowed. Please, - * see {@link CorsConfiguration#setAllowedOrigins(List)} for details. - * By default all origins are allowed unless {@code originPatterns} is - * also set in which case {@code originPatterns} is used instead. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and other considerations. + * + * By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
By default, all origins are allowed, but if + * {@link #allowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence. + * @see #allowedOriginPatterns(String...) */ public CorsRegistration allowedOrigins(String... origins) { this.config.setAllowedOrigins(Arrays.asList(origins)); @@ -58,9 +62,11 @@ public CorsRegistration allowedOrigins(String... origins) { } /** - * Alternative to {@link #allowCredentials} that supports origins declared - * via wildcard patterns. Please, see - * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for details. + * Alternative to {@link #allowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. *
By default this is not set. * @since 5.3 */ @@ -144,7 +150,7 @@ public CorsRegistration maxAge(long maxAge) { * @since 5.3 */ public CorsRegistration combine(CorsConfiguration other) { - this.config.combine(other); + this.config = this.config.combine(other); return this; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..e720174b37ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -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. @@ -118,7 +118,7 @@ private R delegate(Function function) { public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) throws ServletException, IOException { - writeAsync(request, response, createDeferredResult()); + writeAsync(request, response, createDeferredResult(request)); return null; } @@ -140,7 +140,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, } - private DeferredResult createDeferredResult() { + private DeferredResult createDeferredResult(HttpServletRequest request) { DeferredResult result; if (this.timeout != null) { result = new DeferredResult<>(this.timeout.toMillis()); @@ -153,7 +153,13 @@ private DeferredResult createDeferredResult() { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { result.setResult(value); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 44b721e72a2d..fedfe2d4a409 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -361,21 +361,27 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Context context) throws ServletException, IOException { - DeferredResult> deferredResult = createDeferredResult(servletRequest, servletResponse, context); + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } - private DeferredResult> createDeferredResult(HttpServletRequest request, HttpServletResponse response, + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, Context context) { - DeferredResult> result = new DeferredResult<>(); + DeferredResult result = new DeferredResult<>(); entity().handle((value, ex) -> { if (ex != null) { if (ex instanceof CompletionException && ex.getCause() != null) { ex = ex.getCause(); } - result.setErrorResult(ex); + ServerResponse errorResponse = errorResponse(ex, request); + if (errorResponse != null) { + result.setResult(errorResponse); + } + else { + result.setErrorResult(ex); + } } else { try { @@ -468,7 +474,12 @@ public void onNext(T t) { @Override public void onError(Throwable t) { - this.deferredResult.setErrorResult(t); + try { + handleError(t, this.servletRequest, this.servletResponse, this.context); + } + catch (ServletException | IOException handlingThrowable) { + this.deferredResult.setErrorResult(handlingThrowable); + } } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java index 09785c5cf929..9ae67ec10237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ErrorHandlingServerResponse.java @@ -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. @@ -35,7 +35,6 @@ /** * Base class for {@link ServerResponse} implementations with error handling. - * * @author Arjen Poutsma * @since 5.3 */ @@ -55,21 +54,36 @@ protected final void addErrorHandler(Predicate errorHandler : this.errorHandlers) { if (errorHandler.test(t)) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); - ServerResponse serverResponse = errorHandler.handle(t, serverRequest); - return serverResponse.writeTo(servletRequest, servletResponse, context); + return errorHandler.handle(t, serverRequest); } } - throw new ServletException(t); + return null; } - private static class ErrorHandler { private final Predicate predicate; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 98c9f848ec2a..81d38fb3b8c7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -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. @@ -19,10 +19,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -36,6 +38,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,6 +49,7 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -78,9 +82,7 @@ public class HandlerMappingIntrospector @Nullable private List handlerMappings; - @Nullable - private Map pathPatternMatchableHandlerMappings = - new ConcurrentHashMap<>(); + private Map pathPatternHandlerMappings = Collections.emptyMap(); /** @@ -102,7 +104,7 @@ public HandlerMappingIntrospector(ApplicationContext context) { /** - * Return the configured or detected HandlerMapping's. + * Return the configured or detected {@code HandlerMapping}s. */ public List getHandlerMappings() { return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); @@ -119,7 +121,7 @@ public void afterPropertiesSet() { if (this.handlerMappings == null) { Assert.notNull(this.applicationContext, "No ApplicationContext"); this.handlerMappings = initHandlerMappings(this.applicationContext); - this.pathPatternMatchableHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); + this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings); } } @@ -136,51 +138,90 @@ public void afterPropertiesSet() { */ @Nullable public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - Assert.notNull(this.pathPatternMatchableHandlerMappings, "Handler mappings with PathPatterns not initialized"); - HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - Object handler = handlerMapping.getHandler(wrapper); - if (handler == null) { - continue; - } - if (handlerMapping instanceof MatchableHandlerMapping) { - return this.pathPatternMatchableHandlerMappings.getOrDefault( - handlerMapping, (MatchableHandlerMapping) handlerMapping); + HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> { + if (matchedMapping instanceof MatchableHandlerMapping) { + PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping); + if (mapping != null) { + RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest); + return new PathSettingHandlerMapping(mapping, requestPath); + } + else { + String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); + return new PathSettingHandlerMapping((MatchableHandlerMapping) matchedMapping, lookupPath); + } } throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - return null; + }); } @Override @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); - RequestAttributeChangeIgnoringWrapper wrapper = new RequestAttributeChangeIgnoringWrapper(request); - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain handler = null; - try { - handler = handlerMapping.getHandler(wrapper); - } - catch (Exception ex) { - // Ignore + AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request); + return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> { + for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrappedRequest); + } } - if (handler == null) { - continue; + if (executionChain.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) executionChain.getHandler()).getCorsConfiguration(wrappedRequest); } - for (HandlerInterceptor interceptor : handler.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + return null; + }); + } + + @Nullable + private T doWithMatchingMapping( + HttpServletRequest request, boolean ignoreException, + BiFunction matchHandler) throws Exception { + + Assert.notNull(this.handlerMappings, "Handler mappings not initialized"); + + boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty(); + RequestPath previousPath = null; + if (parseRequestPath) { + previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + try { + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain chain = null; + try { + chain = handlerMapping.getHandler(request); + } + catch (Exception ex) { + if (!ignoreException) { + throw ex; + } } + if (chain == null) { + continue; + } + return matchHandler.apply(handlerMapping, chain); } - if (handler.getHandler() instanceof CorsConfigurationSource) { - return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + finally { + if (parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); } } return null; } + @Nullable + private T doWithMatchingMappingIgnoringException( + HttpServletRequest request, BiFunction matchHandler) { + + try { + return doWithMatchingMapping(request, true, matchHandler); + } + catch (Exception ex) { + throw new IllegalStateException("HandlerMapping exception not suppressed", ex); + } + } + private static List initHandlerMappings(ApplicationContext applicationContext) { Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -203,6 +244,7 @@ private static List initFallback(ApplicationContext applicationC catch (IOException ex) { throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); } + String value = props.getProperty(HandlerMapping.class.getName()); String[] names = StringUtils.commaDelimitedListToStringArray(value); List result = new ArrayList<>(names.length); @@ -219,7 +261,7 @@ private static List initFallback(ApplicationContext applicationC return result; } - private static Map initPathPatternMatchableHandlerMappings( + private static Map initPathPatternMatchableHandlerMappings( List mappings) { return mappings.stream() @@ -231,20 +273,83 @@ private static Map initPathPatternMatch /** - * Request wrapper that ignores request attribute changes. + * Request wrapper that buffers request attributes in order protect the + * underlying request from attribute changes. */ - private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + private static class AttributesPreservingRequest extends HttpServletRequestWrapper { + + private final Map attributes; - RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + AttributesPreservingRequest(HttpServletRequest request) { super(request); + this.attributes = initAttributes(request); + } + + private Map initAttributes(HttpServletRequest request) { + Map map = new HashMap<>(); + Enumeration names = request.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + map.put(name, request.getAttribute(name)); + } + return map; } @Override public void setAttribute(String name, Object value) { - // Allow UrlPathHelper-resolved lookupPath to be saved for efficiency - if (name.equals(UrlPathHelper.PATH_ATTRIBUTE)) { - super.setAttribute(name, value); + this.attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public void removeAttribute(String name) { + this.attributes.remove(name); + } + } + + + private static class PathSettingHandlerMapping implements MatchableHandlerMapping { + + private final MatchableHandlerMapping delegate; + + private final Object path; + + private final String pathAttributeName; + + PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) { + this.delegate = delegate; + this.path = path; + this.pathAttributeName = (path instanceof RequestPath ? + ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); + } + + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + Object previousPath = request.getAttribute(this.pathAttributeName); + request.setAttribute(this.pathAttributeName, this.path); + try { + return this.delegate.match(request, pattern); + } + finally { + request.setAttribute(this.pathAttributeName, previousPath); } } + + @Nullable + @Override + public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + return this.delegate.getHandler(request); + } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java index 3a832b001d1b..4b7a906732bb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java @@ -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. @@ -70,4 +70,5 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { return this.delegate.getHandler(request); } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6e96a085974a..1dbc559e2ccf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -36,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -52,7 +51,7 @@ import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.validation.Errors; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.annotation.ValidationAnnotationUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -241,10 +240,8 @@ protected ServletServerHttpRequest createInputMessage(NativeWebRequest webReques protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { - Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { binder.validate(validationHints); break; } diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index 68661676731a..88381315df0d 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -649,8 +649,8 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD */ fun filter(filterFunction: (ServerRequest, (ServerRequest) -> ServerResponse) -> ServerResponse) { builder.filter { request, next -> - filterFunction(request) { - next.handle(request) + filterFunction(request) { handlerRequest -> + next.handle(handlerRequest) } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index f442b2b95518..105496ec02c8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -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. @@ -77,4 +77,24 @@ public void allowCredentials() { .as("Globally origins=\"*\" and allowCredentials=true should be possible") .containsExactly("*"); } + + @Test + void combine() { + CorsConfiguration otherConfig = new CorsConfiguration(); + otherConfig.addAllowedOrigin("http://localhost:3000"); + otherConfig.addAllowedMethod("*"); + otherConfig.applyPermitDefaultValues(); + + this.registry.addMapping("/api/**").combine(otherConfig); + + Map configs = this.registry.getCorsConfigurations(); + assertThat(configs.size()).isEqualTo(1); + CorsConfiguration config = configs.get("/api/**"); + assertThat(config.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:3000")); + assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); + assertThat(config.getExposedHeaders()).isEmpty(); + assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index c6d03c054a3a..745d642b5ad4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -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. @@ -40,6 +40,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; @@ -99,16 +103,6 @@ void detectHandlerMappingsOrdered() { assertThat(actual).isEqualTo(expected); } - void defaultHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.refresh(); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual.size()).isEqualTo(2); - assertThat(actual.get(0).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); - assertThat(actual.get(1).getClass()).isEqualTo(RequestMappingHandlerMapping.class); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void getMatchable(boolean usePathPatterns) throws Exception { @@ -127,16 +121,11 @@ void getMatchable(boolean usePathPatterns) throws Exception { context.refresh(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - - // Initialize the RequestPath. At runtime, ServletRequestPathFilter is expected to do that. - if (usePathPatterns) { - ServletRequestPathUtils.parseAndCache(request); - } - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); assertThat(mapping).isNotNull(); assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); + assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); assertThat(mapping.match(request, "/p*/*")).isNotNull(); assertThat(mapping.match(request, "/b*/*")).isNull(); @@ -156,6 +145,22 @@ void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); } + @Test // gh-26833 + void getMatchablePreservesRequestAttributes() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); + request.setAttribute("name", "value"); + + MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); + assertThat(matchable).isNotNull(); + + // RequestPredicates.restoreAttributes clears and re-adds attributes + assertThat(request.getAttribute("name")).isEqualTo("value"); + } + @Test void getCorsConfigurationPreFlight() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); @@ -209,15 +214,29 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) { @Configuration static class TestConfig { + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + @Bean public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; } @Bean public TestController testController() { return new TestController(); } + + @Bean + public RouterFunction> routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java index cb9e9f2538d8..3f1fce6612a2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java @@ -284,7 +284,7 @@ void classLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } @@ -297,7 +297,7 @@ void methodLevelComposedAnnotation(TestRequestMappingInfoHandlerMapping mapping) CorsConfiguration config = getCorsConfiguration(chain, false); assertThat(config).isNotNull(); assertThat(config.getAllowedMethods()).containsExactly("GET"); - assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example/"); + assertThat(config.getAllowedOrigins()).containsExactly("http://www.foo.example"); assertThat(config.getAllowCredentials()).isTrue(); } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index 7898ded3ed41..750d05d01e3b 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -127,6 +127,13 @@ class RouterFunctionDslTests { } } + @Test + fun filtering() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/filter", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).get().handle(request).headers().getFirst("foo")).isEqualTo("bar") + } + private fun sampleRouter() = router { (GET("/foo/") or GET("/foos/")) { req -> handle(req) } "/api".nest { @@ -160,6 +167,18 @@ class RouterFunctionDslTests { path("/baz", ::handle) GET("/rendering") { RenderingResponse.create("index").build() } add(otherRouter) + add(filterRouter) + } + + private val filterRouter = router { + "/filter" { request -> + ok().header("foo", request.headers().firstHeader("foo")).build() + } + + filter { request, next -> + val newRequest = ServerRequest.from(request).apply { header("foo", "bar") }.build() + next(newRequest) + } } private val otherRouter = router { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index d38d3caa7817..e00ecdb924e5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -43,29 +46,36 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - * When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - * Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
When SockJS is enabled and origins are restricted, transport types that do not - * allow to check request origin (Iframe based transports) are disabled. - * As a consequence, IE 6 to 9 are not supported when origins are restricted. + *
By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(String...) allowedOriginPatterns} is also + * set, then that takes precedence over this property. * - *
Each provided allowed origin must start by "http://", "https://" or be "*" - * (means that all origins are allowed). By default, only same origin requests are - * allowed (empty list). + *
Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + *
By default this is not set. * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java index 48642a305bdf..cf145dd71ae0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketHandlerRegistration.java @@ -16,6 +16,9 @@ package org.springframework.web.socket.config.annotation; +import java.util.List; + +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -45,29 +48,36 @@ public interface WebSocketHandlerRegistration { WebSocketHandlerRegistration addInterceptors(HandshakeInterceptor... interceptors); /** - * Configure allowed {@code Origin} header values. This check is mostly designed for - * browser clients. There is nothing preventing other types of client to modify the - * {@code Origin} header value. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. * - *
Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. * * @since 4.1.2 + * @see #setAllowedOriginPatterns(String...) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ WebSocketHandlerRegistration setAllowedOrigins(String... origins); /** - * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(String...)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + *
By default this is not set. * @since 5.3.5 */ WebSocketHandlerRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 919e2dae8313..245e43340709 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -67,12 +67,23 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + *
By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + *
Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept */ public void setAllowedOrigins(Collection allowedOrigins) { @@ -81,7 +92,7 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return the allowed {@code Origin} header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origins. * @since 4.1.5 */ public Collection getAllowedOrigins() { @@ -91,12 +102,13 @@ public Collection getAllowedOrigins() { } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. + * By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
By default this is not set. * @since 5.3.2 - * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); @@ -104,9 +116,8 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { } /** - * Return the allowed {@code Origin} pattern header values. + * Return the {@link #setAllowedOriginPatterns(Collection) configured} allowed origin patterns. * @since 5.3.2 - * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 66d2522acd62..ac5c2271e494 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -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. @@ -310,17 +310,24 @@ public boolean shouldSuppressCors() { } /** - * Configure allowed {@code Origin} header values. This check is mostly - * designed for browsers. There is nothing preventing other types of client - * to modify the {@code Origin} header value. - * When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - * Each provided allowed origin must have a scheme, and optionally a port - * (e.g. "https://example.org", "https://example.org:9090"). An allowed origin - * string may also be "*" in which case all origins are allowed. + * Set the origins for which cross-origin requests are allowed from a browser. + * Please, refer to {@link CorsConfiguration#setAllowedOrigins(List)} for + * format details and considerations, and keep in mind that the CORS spec + * does not allow use of {@code "*"} with {@code allowCredentials=true}. + * For more flexible origin patterns use {@link #setAllowedOriginPatterns} + * instead. + * + * By default, no origins are allowed. When + * {@link #setAllowedOriginPatterns(Collection) allowedOriginPatterns} is also + * set, then that takes precedence over this property. + * + * Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
When SockJS is enabled and origins are restricted, transport types - * that do not allow to check request origin (Iframe based transports) - * are disabled. As a consequence, IE 6 to 9 are not supported when origins - * are restricted. - *
Note when SockJS is enabled and origins are restricted, transport types + * that do not allow to check request origin (Iframe based transports) are + * disabled. As a consequence, IE 6 to 9 are not supported when origins are + * restricted. + * * @since 4.1.2 + * @see #setAllowedOriginPatterns(Collection) * @see RFC 6454: The Web Origin Concept * @see SockJS supported transports by browser */ @@ -330,19 +337,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Return configure allowed {@code Origin} header values. + * Return the {@link #setAllowedOrigins(Collection) configured} allowed origins. * @since 4.1.2 - * @see #setAllowedOrigins */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOrigins() { return this.corsConfiguration.getAllowedOrigins(); } /** - * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible - * domain patterns, e.g. {@code "https://*.domain1.com"}. Furthermore it - * always sets the {@code Access-Control-Allow-Origin} response header to - * the matched origin and never to {@code "*"}, nor to any other pattern. + * Alternative to {@link #setAllowedOrigins(Collection)} that supports more + * flexible patterns for specifying the origins for which cross-origin + * requests are allowed from a browser. Please, refer to + * {@link CorsConfiguration#setAllowedOriginPatterns(List)} for format + * details and other considerations. * By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity
By default this is not set. * @since 5.2.3 */ @@ -354,7 +361,6 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 - * @see #setAllowedOriginPatterns */ @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1d7e1aa0cbab..4a6ec9023c3e 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,8 @@ + + diff --git a/src/docs/asciidoc/core/core-aop-api.adoc b/src/docs/asciidoc/core/core-aop-api.adoc index 4b7a21573fc2..7c3e40e30c2e 100644 --- a/src/docs/asciidoc/core/core-aop-api.adoc +++ b/src/docs/asciidoc/core/core-aop-api.adoc @@ -57,11 +57,11 @@ The `MethodMatcher` interface is normally more important. The complete interface ---- public interface MethodMatcher { - boolean matches(Method m, Class targetClass); + boolean matches(Method m, Class> targetClass); boolean isRuntime(); - boolean matches(Method m, Class targetClass, Object[] args); + boolean matches(Method m, Class> targetClass, Object... args); } ---- diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index c350ce81710a..d4e4a9a6e7ce 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -316,17 +316,17 @@ other class. They can also contain pointcut, advice, and introduction (inter-typ declarations. .Autodetecting aspects through component scanning -NOTE: You can register aspect classes as regular beans in your Spring XML configuration or -autodetect them through classpath scanning -- the same as any other Spring-managed bean. -However, note that the `@Aspect` annotation is not sufficient for autodetection in -the classpath. For that purpose, you need to add a separate `@Component` annotation -(or, alternatively, a custom stereotype annotation that qualifies, as per the rules of -Spring's component scanner). +NOTE: You can register aspect classes as regular beans in your Spring XML configuration, +via `@Bean` methods in `@Configuration` classes, or have Spring autodetect them through +classpath scanning -- the same as any other Spring-managed bean. However, note that the +`@Aspect` annotation is not sufficient for autodetection in the classpath. For that +purpose, you need to add a separate `@Component` annotation (or, alternatively, a custom +stereotype annotation that qualifies, as per the rules of Spring's component scanner). .Advising aspects with other aspects? -NOTE: In Spring AOP, aspects themselves cannot be the targets of advice -from other aspects. The `@Aspect` annotation on a class marks it as an aspect and, -hence, excludes it from auto-proxying. +NOTE: In Spring AOP, aspects themselves cannot be the targets of advice from other +aspects. The `@Aspect` annotation on a class marks it as an aspect and, hence, excludes +it from auto-proxying. @@ -361,7 +361,7 @@ matches the execution of any method named `transfer`: ---- The pointcut expression that forms the value of the `@Pointcut` annotation is a regular -AspectJ 5 pointcut expression. For a full discussion of AspectJ's pointcut language, see +AspectJ pointcut expression. For a full discussion of AspectJ's pointcut language, see the https://www.eclipse.org/aspectj/doc/released/progguide/index.html[AspectJ Programming Guide] (and, for extensions, the https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html[AspectJ 5 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 9d0d31359255..703765159dad 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -847,12 +847,12 @@ This approach shows that the factory bean itself can be managed and configured t dependency injection (DI). See <>. -NOTE: In Spring documentation, "`factory bean`" refers to a bean that is configured in -the Spring container and that creates objects through an +NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the +Spring container and that creates objects through an <> or <> factory method. By contrast, `FactoryBean` (notice the capitalization) refers to a Spring-specific -<> implementation class. +<> implementation class. [[beans-factory-type-determination]] @@ -3350,8 +3350,9 @@ of the scope. You can also do the `Scope` registration declaratively, by using t ---- -NOTE: When you place `` in a `FactoryBean` implementation, it is the factory -bean itself that is scoped, not the object returned from `getObject()`. +NOTE: When you place `` within a `` declaration for a +`FactoryBean` implementation, it is the factory bean itself that is scoped, not the object +returned from `getObject()`. @@ -4539,22 +4540,22 @@ Java as opposed to a (potentially) verbose amount of XML, you can create your ow `FactoryBean`, write the complex initialization inside that class, and then plug your custom `FactoryBean` into the container. -The `FactoryBean` interface provides three methods: +The `FactoryBean` interface provides three methods: -* `Object getObject()`: Returns an instance of the object this factory creates. The +* `T getObject()`: Returns an instance of the object this factory creates. The instance can possibly be shared, depending on whether this factory returns singletons or prototypes. * `boolean isSingleton()`: Returns `true` if this `FactoryBean` returns singletons or - `false` otherwise. -* `Class getObjectType()`: Returns the object type returned by the `getObject()` method + `false` otherwise. The default implementation of this method returns `true`. +* `Class> getObjectType()`: Returns the object type returned by the `getObject()` method or `null` if the type is not known in advance. -The `FactoryBean` concept and interface is used in a number of places within the Spring +The `FactoryBean` concept and interface are used in a number of places within the Spring Framework. More than 50 implementations of the `FactoryBean` interface ship with Spring itself. When you need to ask a container for an actual `FactoryBean` instance itself instead of -the bean it produces, preface the bean's `id` with the ampersand symbol (`&`) when +the bean it produces, prefix the bean's `id` with the ampersand symbol (`&`) when calling the `getBean()` method of the `ApplicationContext`. So, for a given `FactoryBean` with an `id` of `myBean`, invoking `getBean("myBean")` on the container returns the product of the `FactoryBean`, whereas invoking `getBean("&myBean")` returns the @@ -8237,8 +8238,10 @@ Spring offers a convenient way of working with scoped dependencies through <>. The easiest way to create such a proxy when using the XML configuration is the `` element. Configuring your beans in Java with a `@Scope` annotation offers equivalent support -with the `proxyMode` attribute. The default is no proxy (`ScopedProxyMode.NO`), -but you can specify `ScopedProxyMode.TARGET_CLASS` or `ScopedProxyMode.INTERFACES`. +with the `proxyMode` attribute. The default is `ScopedProxyMode.DEFAULT`, which +typically indicates that no scoped proxy should be created unless a different default +has been configured at the component-scan instruction level. You can specify +`ScopedProxyMode.TARGET_CLASS`, `ScopedProxyMode.INTERFACES` or `ScopedProxyMode.NO`. If you port the scoped proxy example from the XML reference documentation (see <>) to our `@Bean` using Java, @@ -8385,7 +8388,7 @@ annotation, as the following example shows: === Using the `@Configuration` annotation `@Configuration` is a class-level annotation indicating that an object is a source of -bean definitions. `@Configuration` classes declare beans through public `@Bean` annotated +bean definitions. `@Configuration` classes declare beans through `@Bean` annotated methods. Calls to `@Bean` methods on `@Configuration` classes can also be used to define inter-bean dependencies. See <> for a general introduction. @@ -10217,8 +10220,8 @@ bean with the same name. If it does, it uses that bean as the `MessageSource`. I `DelegatingMessageSource` is instantiated in order to be able to accept calls to the methods defined above. -Spring provides two `MessageSource` implementations, `ResourceBundleMessageSource` and -`StaticMessageSource`. Both implement `HierarchicalMessageSource` in order to do nested +Spring provides three `MessageSource` implementations, `ResourceBundleMessageSource`, `ReloadableResourceBundleMessageSource` +and `StaticMessageSource`. All of them implement `HierarchicalMessageSource` in order to do nested messaging. The `StaticMessageSource` is rarely used but provides programmatic ways to add messages to the source. The following example shows `ResourceBundleMessageSource`: diff --git a/src/docs/asciidoc/core/core-expressions.adoc b/src/docs/asciidoc/core/core-expressions.adoc index d445738f5130..c0cd157e2fb2 100644 --- a/src/docs/asciidoc/core/core-expressions.adoc +++ b/src/docs/asciidoc/core/core-expressions.adoc @@ -517,7 +517,7 @@ kinds of expression cannot be compiled at the moment: * Expressions using custom resolvers or accessors * Expressions using selection or projection -More types of expression will be compilable in the future. +More types of expressions will be compilable in the future. @@ -589,7 +589,7 @@ You can also refer to other bean properties by name, as the following example sh To specify a default value, you can place the `@Value` annotation on fields, methods, and method or constructor parameters. -The following example sets the default value of a field variable: +The following example sets the default value of a field: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -788,7 +788,7 @@ using a literal on one side of a logical comparison operator. ---- Numbers support the use of the negative sign, exponential notation, and decimal points. -By default, real numbers are parsed by using Double.parseDouble(). +By default, real numbers are parsed by using `Double.parseDouble()`. @@ -796,10 +796,10 @@ By default, real numbers are parsed by using Double.parseDouble(). === Properties, Arrays, Lists, Maps, and Indexers Navigating with property references is easy. To do so, use a period to indicate a nested -property value. The instances of the `Inventor` class, `pupin` and `tesla`, were populated with -data listed in the <> section. -To navigate "`down`" and get Tesla's year of birth and Pupin's city of birth, we use the following -expressions: +property value. The instances of the `Inventor` class, `pupin` and `tesla`, were +populated with data listed in the <> section. To navigate "down" the object graph and get Tesla's year of birth and +Pupin's city of birth, we use the following expressions: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -939,7 +939,7 @@ You can directly express lists in an expression by using `{}` notation. ---- `{}` by itself means an empty list. For performance reasons, if the list is itself -entirely composed of fixed literals, a constant list is created to represent the +entirely composed of fixed literals, a constant list is created to represent the expression (rather than building a new list on each evaluation). @@ -958,7 +958,7 @@ following example shows how to do so: Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ---- -[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +[source,kotlin,indent=0,subs="verbatim",role="secondary"] .Kotlin ---- // evaluates to a Java map containing the two entries @@ -967,10 +967,11 @@ following example shows how to do so: val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ---- -`{:}` by itself means an empty map. For performance reasons, if the map is itself composed -of fixed literals or other nested constant structures (lists or maps), a constant map is created -to represent the expression (rather than building a new map on each evaluation). Quoting of the map keys -is optional. The examples above do not use quoted keys. +`{:}` by itself means an empty map. For performance reasons, if the map is itself +composed of fixed literals or other nested constant structures (lists or maps), a +constant map is created to represent the expression (rather than building a new map on +each evaluation). Quoting of the map keys is optional (unless the key contains a period +(`.`)). The examples above do not use quoted keys. @@ -1003,8 +1004,7 @@ to have the array populated at construction time. The following example shows ho val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ---- -You cannot currently supply an initializer when you construct -multi-dimensional array. +You cannot currently supply an initializer when you construct a multi-dimensional array. @@ -1105,7 +1105,7 @@ expression-based `matches` operator. The following listing shows examples of bot boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); - //evaluates to false + // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ---- @@ -1120,14 +1120,14 @@ expression-based `matches` operator. The following listing shows examples of bot val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) - //evaluates to false + // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ---- -CAUTION: Be careful with primitive types, as they are immediately boxed up to the wrapper type, -so `1 instanceof T(int)` evaluates to `false` while `1 instanceof T(Integer)` -evaluates to `true`, as expected. +CAUTION: Be careful with primitive types, as they are immediately boxed up to their +wrapper types. For example, `1 instanceof T(int)` evaluates to `false`, while +`1 instanceof T(Integer)` evaluates to `true`, as expected. Each symbolic operator can also be specified as a purely alphabetic equivalent. This avoids problems where the symbols used have special meaning for the document type in @@ -1155,7 +1155,7 @@ SpEL supports the following logical operators: * `or` (`||`) * `not` (`!`) -The following example shows how to use the logical operators +The following example shows how to use the logical operators: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1222,10 +1222,11 @@ The following example shows how to use the logical operators [[expressions-operators-mathematical]] ==== Mathematical Operators -You can use the addition operator on both numbers and strings. You can use the subtraction, multiplication, -and division operators only on numbers. You can also use -the modulus (%) and exponential power (^) operators. Standard operator precedence is enforced. The -following example shows the mathematical operators in use: +You can use the addition operator (`+`) on both numbers and strings. You can use the +subtraction (`-`), multiplication (`*`), and division (`/`) operators only on numbers. +You can also use the modulus (`%`) and exponential power (`^`) operators on numbers. +Standard operator precedence is enforced. The following example shows the mathematical +operators in use: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1296,9 +1297,9 @@ following example shows the mathematical operators in use: [[expressions-assignment]] ==== The Assignment Operator -To setting a property, use the assignment operator (`=`). This is typically -done within a call to `setValue` but can also be done inside a call to `getValue`. The -following listing shows both ways to use the assignment operator: +To set a property, use the assignment operator (`=`). This is typically done within a +call to `setValue` but can also be done inside a call to `getValue`. The following +listing shows both ways to use the assignment operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1333,9 +1334,9 @@ You can use the special `T` operator to specify an instance of `java.lang.Class` type). Static methods are invoked by using this operator as well. The `StandardEvaluationContext` uses a `TypeLocator` to find types, and the `StandardTypeLocator` (which can be replaced) is built with an understanding of the -`java.lang` package. This means that `T()` references to types within `java.lang` do not need to be -fully qualified, but all other type references must be. The following example shows how -to use the `T` operator: +`java.lang` package. This means that `T()` references to types within the `java.lang` +package do not need to be fully qualified, but all other type references must be. The +following example shows how to use the `T` operator: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1365,9 +1366,10 @@ to use the `T` operator: [[expressions-constructors]] === Constructors -You can invoke constructors by using the `new` operator. You should use the fully qualified class name -for all but the primitive types (`int`, `float`, and so on) and String. The following -example shows how to use the `new` operator to invoke constructors: +You can invoke constructors by using the `new` operator. You should use the fully +qualified class name for all types except those located in the `java.lang` package +(`Integer`, `Float`, `String`, and so on). The following example shows how to use the +`new` operator to invoke constructors: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1376,7 +1378,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); @@ -1388,7 +1390,7 @@ example shows how to use the `new` operator to invoke constructors: "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) - //create new inventor instance within add method of List + // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) @@ -1802,7 +1804,7 @@ Selection is a powerful expression language feature that lets you transform a source collection into another collection by selecting from its entries. Selection uses a syntax of `.?[selectionExpression]`. It filters the collection and -returns a new collection that contain a subset of the original elements. For example, +returns a new collection that contains a subset of the original elements. For example, selection lets us easily get a list of Serbian inventors, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] @@ -1818,14 +1820,14 @@ selection lets us easily get a list of Serbian inventors, as the following examp "members.?[nationality == 'Serbian']").getValue(societyContext) as List ---- -Selection is possible upon both lists and maps. For a list, the selection -criteria is evaluated against each individual list element. Against a map, the -selection criteria is evaluated against each map entry (objects of the Java type -`Map.Entry`). Each map entry has its key and value accessible as properties for use in -the selection. +Selection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. For a list or array, the selection criteria is evaluated against each +individual element. Against a map, the selection criteria is evaluated against each map +entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` +accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the original map -where the entry value is less than 27: +The following expression returns a new map that consists of those elements of the +original map where the entry's value is less than 27: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1838,9 +1840,8 @@ where the entry value is less than 27: val newMap = parser.parseExpression("map.?[value<27]").getValue() ---- - -In addition to returning all the selected elements, you can retrieve only the -first or the last value. To obtain the first entry matching the selection, the syntax is +In addition to returning all the selected elements, you can retrieve only the first or +the last element. To obtain the first element matching the selection, the syntax is `.^[selectionExpression]`. To obtain the last matching selection, the syntax is `.$[selectionExpression]`. @@ -1849,11 +1850,11 @@ first or the last value. To obtain the first entry matching the selection, the s [[expressions-collection-projection]] === Collection Projection -Projection lets a collection drive the evaluation of a sub-expression, and the -result is a new collection. The syntax for projection is `.![projectionExpression]`. For -example, suppose we have a list of inventors but want the list of -cities where they were born. Effectively, we want to evaluate 'placeOfBirth.city' for -every entry in the inventor list. The following example uses projection to do so: +Projection lets a collection drive the evaluation of a sub-expression, and the result is +a new collection. The syntax for projection is `.![projectionExpression]`. For example, +suppose we have a list of inventors but want the list of cities where they were born. +Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +list. The following example uses projection to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1868,7 +1869,8 @@ every entry in the inventor list. The following example uses projection to do so val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ---- -You can also use a map to drive projection and, in this case, the projection expression is +Projection is supported for arrays and anything that implements `java.lang.Iterable` or +`java.util.Map`. When using a map to drive projection, the projection expression is evaluated against each entry in the map (represented as a Java `Map.Entry`). The result of a projection across a map is a list that consists of the evaluation of the projection expression against each map entry. diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index 872d14ae2feb..82c9b0d2f94a 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -103,7 +103,7 @@ example implements `Validator` for `Person` instances: ---- class PersonValidator : Validator { - /** + /\** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { @@ -500,8 +500,9 @@ the various `PropertyEditor` implementations that Spring provides: | `LocaleEditor` | Can resolve strings to `Locale` objects and vice-versa (the string format is - `[language]_[country]_[variant]`, same as the `toString()` method of - `Locale`). By default, registered by `BeanWrapperImpl`. + `[language]\_[country]_[variant]`, same as the `toString()` method of + `Locale`). Also accepts spaces as separators, as an alternative to underscores. + By default, registered by `BeanWrapperImpl`. | `PatternEditor` | Can resolve strings to `java.util.regex.Pattern` objects and vice-versa. @@ -541,10 +542,9 @@ com Note that you can also use the standard `BeanInfo` JavaBeans mechanism here as well (described to some extent -https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[ -here]). The following example use the `BeanInfo` mechanism to -explicitly register one or more `PropertyEditor` instances with the properties of an -associated class: +https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html[here]). The +following example uses the `BeanInfo` mechanism to explicitly register one or more +`PropertyEditor` instances with the properties of an associated class: [literal,subs="verbatim,quotes"] ---- @@ -567,9 +567,10 @@ associates a `CustomNumberEditor` with the `age` property of the `Something` cla try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; - }; + } }; return new PropertyDescriptor[] { ageDescriptor }; } @@ -625,7 +626,7 @@ nested property setup, so we strongly recommend that you use it with the where it can be automatically detected and applied. Note that all bean factories and application contexts automatically use a number of -built-in property editors, through their use a `BeanWrapper` to +built-in property editors, through their use of a `BeanWrapper` to handle property conversions. The standard property editors that the `BeanWrapper` registers are listed in the <>. Additionally, `ApplicationContexts` also override or add additional editors to handle @@ -1492,13 +1493,17 @@ The following listing shows the `FormatterRegistry` SPI: public interface FormatterRegistry extends ConverterRegistry { - void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); + void addPrinter(Printer> printer); + + void addParser(Parser> parser); + + void addFormatter(Formatter> formatter); void addFormatterForFieldType(Class> fieldType, Formatter> formatter); - void addFormatterForFieldType(Formatter> formatter); + void addFormatterForFieldType(Class> fieldType, Printer> printer, Parser> parser); - void addFormatterForAnnotation(AnnotationFormatterFactory> factory); + void addFormatterForFieldAnnotation(AnnotationFormatterFactory extends Annotation> annotationFormatterFactory); } ---- diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index cb2901e8ce4c..1a305273ecf3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -1,6 +1,9 @@ = Spring Framework Documentation :doc-root: https://docs.spring.io +:github-repo: spring-projects/spring-framework + :api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:spring-framework-main-code: https://github.com/{github-repo}/tree/main **** _What's New_, _Upgrade Notes_, _Supported Versions_, and other topics, diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index c529ebb75584..bffaf7672236 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -163,7 +163,7 @@ You can use the `exchange()` methods to specify request headers, as the followin URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); RequestEntity requestEntity = RequestEntity.get(uri) - .header(("MyRequestHeader", "MyValue") + .header("MyRequestHeader", "MyValue") .build(); ResponseEntity