From 2a8c369cffd4614e7c06b7ee5387f135dd29d38d Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 31 Mar 2022 09:04:49 +0000 Subject: [PATCH 01/64] Next development version (v5.3.19-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 75c9b894bcfa..03750b799538 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.18-SNAPSHOT +version=5.3.19-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From 24cd3c1f4cfe34c17b79da1dd383112981f99de4 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 1 Apr 2022 16:07:05 +0100 Subject: [PATCH 02/64] Revert "Disable flaky integration tests for now" This reverts commit 1627f57f1f77abe17dd607c75476b9e4cb22ffbb in preparation for fixing the root cause --- .../result/method/annotation/MultipartIntegrationTests.java | 2 -- .../result/method/annotation/CoroutinesIntegrationTests.kt | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java index 9f83dff6601b..b60587452ac1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.jupiter.api.Disabled; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -63,7 +62,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeFalse; -@Disabled class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTests { private WebClient webClient; diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt index 56e25eb9fc51..18ab6e1dda78 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.jupiter.api.Disabled import org.springframework.context.ApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan @@ -40,7 +39,6 @@ import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpSe import reactor.core.publisher.Flux import java.time.Duration -@Disabled class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { override fun initApplicationContext(): ApplicationContext { From d518a7d8c81e2cf7484942c37836219b3669240d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 1 Apr 2022 17:36:14 +0100 Subject: [PATCH 03/64] AbstractListenerReadPublisher continues after 0 bytes If we read 0 bytes, e.g. chunked encoding markup read but not the actual data within it, don't stop reading since the server may or may not consider it necessary to call onDataAvailable again. Instead, we keep on reading, and although isReady likely returns false on the next iteration, it eliminates ambiguity and ensures the server will call onDataAvailable when more data arrives. Closes gh-28241 --- .../reactive/AbstractListenerReadPublisher.java | 15 ++++++++++++--- .../server/reactive/ServletServerHttpRequest.java | 10 +++++----- 2 files changed, 17 insertions(+), 8 deletions(-) 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 0845a9f25f04..de1f3ca5a600 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Operators; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.log.LogDelegateFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -56,6 +58,8 @@ public abstract class AbstractListenerReadPublisher implements Publisher { */ protected static Log rsReadLogger = LogDelegateFactory.getHiddenLog(AbstractListenerReadPublisher.class); + final static DataBuffer EMPTY_BUFFER = DefaultDataBufferFactory.sharedInstance.allocateBuffer(0); + private final AtomicReference state = new AtomicReference<>(State.UNSUBSCRIBED); @@ -180,7 +184,7 @@ public final void onError(Throwable ex) { /** * Read and publish data one at a time until there is no more data, no more - * demand, or perhaps we completed in the mean time. + * demand, or perhaps we completed meanwhile. * @return {@code true} if there is more demand; {@code false} if there is * no more demand or we have completed. */ @@ -188,7 +192,12 @@ private boolean readAndPublish() throws IOException { long r; while ((r = this.demand) > 0 && (this.state.get() != State.COMPLETED)) { T data = read(); - if (data != null) { + if (data == EMPTY_BUFFER) { + if (rsReadLogger.isTraceEnabled()) { + rsReadLogger.trace(getLogPrefix() + "0 bytes read, trying again"); + } + } + else if (data != null) { if (r != Long.MAX_VALUE) { DEMAND_FIELD_UPDATER.addAndGet(this, -1L); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index a84ddc6d6e3d..7183234e1d4a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -236,10 +236,10 @@ AsyncListener getAsyncListener() { /** * Read from the request body InputStream and return a DataBuffer. * Invoked only when {@link ServletInputStream#isReady()} returns "true". - * @return a DataBuffer with data read, or {@link #EOF_BUFFER} if the input - * stream returned -1, or null if 0 bytes were read. + * @return a DataBuffer with data read, or + * {@link AbstractListenerReadPublisher#EMPTY_BUFFER} if 0 bytes were read, + * or {@link #EOF_BUFFER} if the input stream returned -1. */ - @Nullable DataBuffer readFromInputStream() throws IOException { int read = this.request.getInputStream().read(this.buffer); logBytesRead(read); @@ -254,7 +254,7 @@ DataBuffer readFromInputStream() throws IOException { return EOF_BUFFER; } - return null; + return AbstractListenerReadPublisher.EMPTY_BUFFER; } protected final void logBytesRead(int read) { From 17f7a241184a7955d92ff04456400fb367755f62 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 1 Apr 2022 19:15:23 +0200 Subject: [PATCH 04/64] Add application/graphql+json mime and media types Closes gh-28271 --- .../java/org/springframework/util/MimeTypeUtils.java | 12 ++++++++++++ .../java/org/springframework/http/MediaType.java | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 05809bc5ad7b..c7c5468b5896 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -65,6 +65,17 @@ public abstract class MimeTypeUtils { */ public static final String ALL_VALUE = "*/*"; + /** + * Public constant mime type for {@code application/graphql+json}. + * @see GraphQL over HTTP spec + * */ + public static final MimeType APPLICATION_GRAPHQL; + + /** + * A String equivalent of {@link MimeTypeUtils#APPLICATION_GRAPHQL}. + */ + public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; + /** * Public constant mime type for {@code application/json}. * */ @@ -165,6 +176,7 @@ public abstract class MimeTypeUtils { static { // Not using "parseMimeType" to avoid static init cost ALL = new MimeType("*", "*"); + APPLICATION_GRAPHQL = new MimeType("application", "graphql+json"); APPLICATION_JSON = new MimeType("application", "json"); APPLICATION_OCTET_STREAM = new MimeType("application", "octet-stream"); APPLICATION_XML = new MimeType("application", "xml"); diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 729555add951..2a784e0386b1 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -95,6 +95,17 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded"; + /** + * Public constant media type for {@code application/graphql+json}. + * @see GraphQL over HTTP spec + */ + public static final MediaType APPLICATION_GRAPHQL; + + /** + * A String equivalent of {@link MediaType#APPLICATION_GRAPHQL}. + */ + public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; + /** * Public constant media type for {@code application/json}. */ @@ -396,6 +407,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_ATOM_XML = new MediaType("application", "atom+xml"); APPLICATION_CBOR = new MediaType("application", "cbor"); APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded"); + APPLICATION_GRAPHQL = new MediaType("application", "graphql+json"); APPLICATION_JSON = new MediaType("application", "json"); APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8); APPLICATION_NDJSON = new MediaType("application", "x-ndjson"); From b158110801db32b50243be538165274551a49512 Mon Sep 17 00:00:00 2001 From: GatinMI Date: Mon, 28 Feb 2022 23:29:28 +0300 Subject: [PATCH 05/64] Fix debug log for no matching acceptableTypes --- .../annotation/AbstractMessageConverterMethodProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 158a33b9c918..8152b9194744 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -242,12 +242,12 @@ protected void writeWithMessageConverters(@Nullable T value, MethodParameter } } if (mediaTypesToUse.isEmpty()) { - if (body != null) { - throw new HttpMediaTypeNotAcceptableException(producibleTypes); - } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } + if (body != null) { + throw new HttpMediaTypeNotAcceptableException(producibleTypes); + } return; } From 270b167e083a6b8461d2d865c794e922180fc567 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2022 19:01:03 +0200 Subject: [PATCH 06/64] Upgrade registry-image-resource in CI pipeline to 1.5.0 See spring-projects/spring-boot#30408 --- ci/pipeline.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index eb8de81ea1e1..712bd2cac0de 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -41,6 +41,11 @@ anchors: GITHUB_TOKEN: ((github-ci-release-token)) resource_types: +- name: registry-image + type: registry-image + source: + repository: concourse/registry-image-resource + tag: 1.5.0 - name: artifactory-resource type: registry-image source: From 01fd489b47f8f2a3e1fdb5a3779c930c29e9ee57 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 7 Apr 2022 08:51:48 +0200 Subject: [PATCH 07/64] Upgrade Ubuntu version in CI images --- ci/images/ci-image/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index ac4db911a85f..37ab9b6edb47 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal-20220302 +FROM ubuntu:focal-20220404 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh From 4e9af3e277e64ff6bacb9a925c9058b7d17d3400 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 7 Apr 2022 08:52:43 +0200 Subject: [PATCH 08/64] Upgrade Java 18 in CI image --- ci/images/get-jdk-url.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index d090494f6627..3b0006dd7f6d 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -12,7 +12,7 @@ case "$1" in echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz" ;; java18) - echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk18-2022-02-12-08-06-beta/OpenJDK18-jdk_x64_linux_hotspot_2022-02-12-08-06.tar.gz" + echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz" ;; *) echo $"Unknown java version" From 0cf15c0fdd54a894e518928809600fdf5d87f0dd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:00:20 +0200 Subject: [PATCH 09/64] Upgrade to Tomcat 9.0.62, Jetty 9.4.46, Jetty Reactive HttpClient 1.1.11, Undertow 2.2.17, R2DBC Arabba-SR13, RSocket 1.1.2, OpenPDF 1.3.27, HtmlUnit 2.60, Checkstyle 10.1 --- build.gradle | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index d21c458e1fcb..98f15755263a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,9 +30,9 @@ configure(allprojects) { project -> mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6" mavenBom "io.netty:netty-bom:4.1.75.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.17" - mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR12" - mavenBom "io.rsocket:rsocket-bom:1.1.1" - mavenBom "org.eclipse.jetty:jetty-bom:9.4.45.v20220203" + mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" + mavenBom "io.rsocket:rsocket-bom:1.1.2" + mavenBom "org.eclipse.jetty:jetty-bom:9.4.46.v20220331" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2" mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2" @@ -96,7 +96,7 @@ configure(allprojects) { project -> dependency "com.h2database:h2:2.1.210" dependency "com.github.ben-manes.caffeine:caffeine:2.9.3" - dependency "com.github.librepdf:openpdf:1.3.26" + dependency "com.github.librepdf:openpdf:1.3.27" dependency "com.rometools:rome:1.18.0" dependency "commons-io:commons-io:2.5" dependency "io.vavr:vavr:0.10.4" @@ -128,18 +128,18 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.48" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.60') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.62') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.60') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.62') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } - dependencySet(group: 'io.undertow', version: '2.2.16.Final') { + dependencySet(group: 'io.undertow', version: '2.2.17.Final') { entry 'undertow-core' entry('undertow-servlet') { exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec" @@ -150,7 +150,7 @@ configure(allprojects) { project -> } } - dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.10" + dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.11" dependency 'org.apache.httpcomponents.client5:httpclient5:5.1.3' dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.1.3' dependency("org.apache.httpcomponents:httpclient:4.5.13") { @@ -206,10 +206,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.12.1" - dependency("net.sourceforge.htmlunit:htmlunit:2.59.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.60.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.59.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.60.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "9.3" + toolVersion = "10.1" configDirectory.set(rootProject.file("src/checkstyle")) } From 90103b0ae99361a21a21c5306fa85b1614a4e2fb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:00:31 +0200 Subject: [PATCH 10/64] Consistent support for direct column matches in DataClassRowMapper Closes gh-28243 --- .../jdbc/core/DataClassRowMapper.java | 15 +++++++++--- .../jdbc/core/AbstractRowMapperTests.java | 24 +++++++++++++++---- .../jdbc/core/BeanPropertyRowMapperTests.java | 11 +++++++++ .../jdbc/core/DataClassRowMapperTests.java | 10 ++++---- .../jdbc/core/test/AbstractPerson.java | 12 +++++----- .../test/ConstructorPersonWithGenerics.java | 10 ++++---- .../test/ConstructorPersonWithSetters.java | 16 ++++++------- .../jdbc/core/test/SpacePerson.java | 6 ++--- 8 files changed, 69 insertions(+), 35 deletions(-) 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 7e09fcbe8919..274bfdfa0cbf 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-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,9 +98,18 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { - String name = underscoreName(this.constructorParameterNames[i]); + String name = this.constructorParameterNames[i]; + int index; + try { + // Try direct name match first + index = rs.findColumn(lowerCaseName(name)); + } + catch (SQLException ex) { + // Try underscored name match instead + index = rs.findColumn(underscoreName(name)); + } TypeDescriptor td = this.constructorParameterTypes[i]; - Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + Object value = getColumnValue(rs, index, td.getType()); args[i] = tc.convertIfNecessary(value, td.getType(), td); } } 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 1c0a86ffed97..cf9d5817b0c0 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 @@ -20,6 +20,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Timestamp; @@ -63,7 +64,7 @@ protected void verifyPerson(Person person) { protected void verifyPerson(ConcretePerson person) { assertThat(person.getName()).isEqualTo("Bubba"); assertThat(person.getAge()).isEqualTo(22L); - assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBirthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); verifyPersonViaBeanWrapper(person); } @@ -94,7 +95,14 @@ private void verifyPersonViaBeanWrapper(Object person) { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(person); assertThat(bw.getPropertyValue("name")).isEqualTo("Bubba"); assertThat(bw.getPropertyValue("age")).isEqualTo(22L); - assertThat((Date) bw.getPropertyValue("birth_date")).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + Date birthDate; + if (bw.isReadableProperty("birth_date")) { + birthDate = (Date) bw.getPropertyValue("birth_date"); + } + else { + birthDate = (Date) bw.getPropertyValue("birthDate"); + } + assertThat(birthDate).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(bw.getPropertyValue("balance")).isEqualTo(new BigDecimal("1234.56")); } @@ -107,7 +115,7 @@ protected void verifyPerson(EmailPerson person) { } - protected enum MockType {ONE, TWO, THREE} + protected enum MockType {ONE, TWO, THREE, FOUR} protected static class Mock { @@ -152,13 +160,19 @@ public Mock(MockType type) throws Exception { given(resultSetMetaData.getColumnLabel(1)).willReturn( type == MockType.THREE ? "Last Name" : "name"); given(resultSetMetaData.getColumnLabel(2)).willReturn("age"); - given(resultSetMetaData.getColumnLabel(3)).willReturn("birth_date"); + given(resultSetMetaData.getColumnLabel(3)).willReturn(type == MockType.FOUR ? "birthdate" :"birth_date"); given(resultSetMetaData.getColumnLabel(4)).willReturn("balance"); given(resultSetMetaData.getColumnLabel(5)).willReturn("e_mail"); given(resultSet.findColumn("name")).willReturn(1); given(resultSet.findColumn("age")).willReturn(2); - given(resultSet.findColumn("birth_date")).willReturn(3); + if (type == MockType.FOUR) { + given(resultSet.findColumn("birthdate")).willReturn(3); + } + else { + given(resultSet.findColumn("birthdate")).willThrow(new SQLException()); + given(resultSet.findColumn("birth_date")).willReturn(3); + } given(resultSet.findColumn("balance")).willReturn(4); given(resultSet.findColumn("e_mail")).willReturn(5); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 99e9eb416274..5ef1f57f8916 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -140,6 +140,17 @@ void queryWithSpaceInColumnNameAndLocalDate() throws Exception { mock.verifyClosed(); } + @Test + void queryWithDirectNameMatchOnBirthDate() throws Exception { + Mock mock = new Mock(MockType.FOUR); + List result = mock.getJdbcTemplate().query( + "select name, age, birthdate, balance from people", + new BeanPropertyRowMapper<>(ConcretePerson.class)); + assertThat(result).hasSize(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + @Test void queryWithUnderscoreInColumnNameAndPersonWithMultipleAdjacentUppercaseLettersInPropertyName() throws Exception { Mock mock = new Mock(); 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 48b0f7f03134..c612e5bcae63 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-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public void testStaticQueryWithDataClassAndGenerics() throws Exception { 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.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); mock.verifyClosed(); @@ -65,15 +65,15 @@ public void testStaticQueryWithDataClassAndGenerics() throws Exception { @Test public void testStaticQueryWithDataClassAndSetters() throws Exception { - Mock mock = new Mock(); + Mock mock = new Mock(MockType.FOUR); List result = mock.getJdbcTemplate().query( - "select name, age, birth_date, balance from people", + "select name, age, birthdate, balance from people", new DataClassRowMapper<>(ConstructorPersonWithSetters.class)); assertThat(result.size()).isEqualTo(1); ConstructorPersonWithSetters 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.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); mock.verifyClosed(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java index f2698d3073ac..b084644c6896 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ public abstract class AbstractPerson { private long age; - private Date birth_date; + private Date birthDate; public String getName() { @@ -46,12 +46,12 @@ public void setAge(long age) { this.age = age; } - public Date getBirth_date() { - return birth_date; + public Date getBirthDate() { + return birthDate; } - public void setBirth_date(Date birth_date) { - this.birth_date = birth_date; + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; } } 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 index 3ae8e271c810..289197b56392 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public class ConstructorPersonWithGenerics { private final long age; - private final Date birth_date; + private final Date birthDate; private final List balance; @@ -37,7 +37,7 @@ public class ConstructorPersonWithGenerics { public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { this.name = name; this.age = age; - this.birth_date = birth_date; + this.birthDate = birth_date; this.balance = balance; } @@ -50,8 +50,8 @@ public long age() { return this.age; } - public Date birth_date() { - return this.birth_date; + public Date birthDate() { + return this.birthDate; } public List balance() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java index ef1feb9a324d..0776b5cc48ab 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author 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,15 +28,15 @@ public class ConstructorPersonWithSetters { private long age; - private Date birth_date; + private Date birthDate; private BigDecimal balance; - public ConstructorPersonWithSetters(String name, long age, Date birth_date, BigDecimal balance) { + public ConstructorPersonWithSetters(String name, long age, Date birthDate, BigDecimal balance) { this.name = name.toUpperCase(); this.age = age; - this.birth_date = birth_date; + this.birthDate = birthDate; this.balance = balance; } @@ -49,8 +49,8 @@ public void setAge(long age) { this.age = age; } - public void setBirth_date(Date birth_date) { - this.birth_date = birth_date; + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; } public void setBalance(BigDecimal balance) { @@ -65,8 +65,8 @@ public long age() { return this.age; } - public Date birth_date() { - return this.birth_date; + public Date birthDate() { + return this.birthDate; } public BigDecimal balance() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java index 8dc8875e15c8..2fc59db1b2e6 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,8 +60,8 @@ public BigDecimal getBalance() { return balance; } - public void setBalance(BigDecimal balanace) { - this.balance = balanace; + public void setBalance(BigDecimal balance) { + this.balance = balance; } } From c3fe112fd7517cc350a868d3af9995d189cbdfb6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:00:50 +0200 Subject: [PATCH 11/64] Consistent use of getLocalAddr() without DNS lookups in request adapters Closes gh-28280 --- .../springframework/http/server/ServletServerHttpRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index 82f722310c3b..45ce146f2ef8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -197,7 +197,7 @@ public Principal getPrincipal() { @Override public InetSocketAddress getLocalAddress() { - return new InetSocketAddress(this.servletRequest.getLocalName(), this.servletRequest.getLocalPort()); + return new InetSocketAddress(this.servletRequest.getLocalAddr(), this.servletRequest.getLocalPort()); } @Override From 7aed6279a284283f079290f44c5d0eee7bef7cda Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:02:36 +0200 Subject: [PATCH 12/64] Consistent fallback in case of fast-class generation failure Closes gh-28138 --- .../aop/framework/CglibAopProxy.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 022cc0fddf24..87fa84d6b98a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -375,6 +375,22 @@ private static boolean implementsInterface(Method method, Set> ifcs) { return false; } + /** + * Invoke the given method with a CGLIB MethodProxy if possible, falling back + * to a plain reflection invocation in case of a fast-class generation failure. + */ + @Nullable + private static Object invokeMethod(@Nullable Object target, Method method, Object[] args, MethodProxy methodProxy) + throws Throwable { + try { + return methodProxy.invoke(target, args); + } + catch (CodeGenerationException ex) { + CglibMethodInvocation.logFastClassGenerationFailure(method); + return AopUtils.invokeJoinpointUsingReflection(target, method, args); + } + } + /** * Process a return value. Wraps a return of {@code this} if necessary to be the * {@code proxy} and also verifies that {@code null} is not returned as a primitive. @@ -425,7 +441,7 @@ public StaticUnadvisedInterceptor(@Nullable Object target) { @Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - Object retVal = methodProxy.invoke(this.target, args); + Object retVal = invokeMethod(this.target, method, args, methodProxy); return processReturnType(proxy, this.target, method, retVal); } } @@ -450,7 +466,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object oldProxy = null; try { oldProxy = AopContext.setCurrentProxy(proxy); - Object retVal = methodProxy.invoke(this.target, args); + Object retVal = invokeMethod(this.target, method, args, methodProxy); return processReturnType(proxy, this.target, method, retVal); } finally { @@ -478,7 +494,7 @@ public DynamicUnadvisedInterceptor(TargetSource targetSource) { public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object target = this.targetSource.getTarget(); try { - Object retVal = methodProxy.invoke(target, args); + Object retVal = invokeMethod(target, method, args, methodProxy); return processReturnType(proxy, target, method, retVal); } finally { @@ -508,7 +524,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy Object target = this.targetSource.getTarget(); try { oldProxy = AopContext.setCurrentProxy(proxy); - Object retVal = methodProxy.invoke(target, args); + Object retVal = invokeMethod(target, method, args, methodProxy); return processReturnType(proxy, target, method, retVal); } finally { @@ -685,13 +701,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); - try { - retVal = methodProxy.invoke(target, argsToUse); - } - catch (CodeGenerationException ex) { - CglibMethodInvocation.logFastClassGenerationFailure(method); - retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); - } + retVal = invokeMethod(target, method, argsToUse, methodProxy); } else { // We need to create a method invocation... From 9f9116839681909388fd87bf481a3d5cd353c9aa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:02:53 +0200 Subject: [PATCH 13/64] Restore ability to configure setClassLoader methods Closes gh-28269 --- .../beans/CachedIntrospectionResults.java | 17 ++++++----- .../beans/BeanWrapperTests.java | 28 +++++++++++++++++-- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 4187097ce371..2929509d2343 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -287,13 +287,15 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { // This call is slow so we do it once. PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { - if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) { + if (Class.class == beanClass && !("name".equals(pd.getName()) || + (pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) { // Only allow all name variants of Class properties continue; } - if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) - || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) { - // Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those + if (pd.getWriteMethod() == null && pd.getPropertyType() != null && + (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) || + ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) { + // Ignore ClassLoader and ProtectionDomain read-only properties - no need to bind to those continue; } if (logger.isTraceEnabled()) { @@ -342,9 +344,10 @@ private void introspectInterfaces(Class beanClass, Class currClass, Set assertThat(ex.getPossibleMatches()).containsExactly("age")); } - @Test // Can't be shared; there is no such thing as a read-only field + @Test // Can't be shared; there is no such thing as a read-only field void setReadOnlyMapProperty() { TypedReadOnlyMap map = new TypedReadOnlyMap(Collections.singletonMap("key", new TestBean())); TypedReadOnlyMapClient target = new TypedReadOnlyMapClient(); @@ -157,12 +159,34 @@ void propertyDescriptors() { BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("name", "a"); accessor.setPropertyValue("spouse.name", "b"); + assertThat(target.getName()).isEqualTo("a"); assertThat(target.getSpouse().getName()).isEqualTo("b"); assertThat(accessor.getPropertyValue("name")).isEqualTo("a"); assertThat(accessor.getPropertyValue("spouse.name")).isEqualTo("b"); assertThat(accessor.getPropertyDescriptor("name").getPropertyType()).isEqualTo(String.class); assertThat(accessor.getPropertyDescriptor("spouse.name").getPropertyType()).isEqualTo(String.class); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.getPropertyValue("class.name")).isEqualTo(TestBean.class.getName()); + assertThat(accessor.getPropertyValue("class.simpleName")).isEqualTo(TestBean.class.getSimpleName()); + assertThat(accessor.getPropertyDescriptor("class.name").getPropertyType()).isEqualTo(String.class); + assertThat(accessor.getPropertyDescriptor("class.simpleName").getPropertyType()).isEqualTo(String.class); + + accessor = createAccessor(new DefaultResourceLoader()); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("classLoader")).isTrue(); + assertThat(accessor.isWritableProperty("classLoader")).isTrue(); + OverridingClassLoader ocl = new OverridingClassLoader(getClass().getClassLoader()); + accessor.setPropertyValue("classLoader", ocl); + assertThat(accessor.getPropertyValue("classLoader")).isSameAs(ocl); } @Test From eefdd2c768cbacdc97839ede2209a630cf1f0d3e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:03:13 +0200 Subject: [PATCH 14/64] Avoid return value reference in potentially cached MethodParameter instance Closes gh-28232 --- .../messaging/handler/HandlerMethod.java | 10 +++++----- .../org/springframework/web/method/HandlerMethod.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index 4ace844929fb..7a834c5d2f07 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -420,21 +420,21 @@ public HandlerMethodParameter clone() { private class ReturnValueMethodParameter extends HandlerMethodParameter { @Nullable - private final Object returnValue; + private final Class returnValueType; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); - this.returnValue = returnValue; + this.returnValueType = (returnValue != null ? returnValue.getClass() : null); } protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { super(original); - this.returnValue = original.returnValue; + this.returnValueType = original.returnValueType; } @Override public Class getParameterType() { - return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); + return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 80d6e7999eb4..ca859130f69f 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -582,21 +582,21 @@ public HandlerMethodParameter clone() { private class ReturnValueMethodParameter extends HandlerMethodParameter { @Nullable - private final Object returnValue; + private final Class returnValueType; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); - this.returnValue = returnValue; + this.returnValueType = (returnValue != null ? returnValue.getClass() : null); } protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { super(original); - this.returnValue = original.returnValue; + this.returnValueType = original.returnValueType; } @Override public Class getParameterType() { - return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); + return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); } @Override From 4143b445d6328040e3697208ed3ac4a3a15cb2cb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 13:04:14 +0200 Subject: [PATCH 15/64] Polishing --- .../jmx/export/notificationPublisherTests.xml | 8 ++++---- .../org/springframework/core/convert/Property.java | 4 ++-- .../expression/spel/ast/ConstructorReference.java | 4 ++-- .../http/codec/json/Jackson2JsonEncoderTests.java | 11 ++++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml index 8b8699a8e289..9e7c75135c21 100644 --- a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml +++ b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml @@ -5,16 +5,16 @@ - + - + - - + + diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java index 5cddaea8769f..bf9ae585181a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/Property.java +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ */ public final class Property { - private static Map annotationCache = new ConcurrentReferenceHashMap<>(); + private static final Map annotationCache = new ConcurrentReferenceHashMap<>(); private final Class objectType; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index 6f4f3c8c69cc..c425c84746ea 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -288,8 +288,8 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException else { // There is an initializer if (this.dimensions == null || this.dimensions.length > 1) { - // There is an initializer but this is a multi-dimensional array (e.g. new int[][]{{1,2},{3,4}}) - this - // is not currently supported + // There is an initializer but this is a multi-dimensional array (e.g. new int[][]{{1,2},{3,4}}) + // - this is not currently supported throw new SpelEvaluationException(getStartPosition(), SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java index 2e589ac2a8f8..b66084e8299a 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java @@ -103,7 +103,7 @@ public void encode() throws Exception { ); } - @Test // SPR-15866 + @Test // SPR-15866 public void canEncodeWithCustomMimeType() { MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8); Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(new ObjectMapper(), textJavascript); @@ -231,9 +231,8 @@ public void jacksonValue() { ); } - @Test // gh-28045 + @Test // gh-28045 public void jacksonValueUnwrappedBeforeObjectMapperSelection() { - JacksonViewBean bean = new JacksonViewBean(); bean.setWithView1("with"); bean.setWithView2("with"); @@ -248,13 +247,15 @@ public void jacksonValueUnwrappedBeforeObjectMapperSelection() { ObjectMapper mapper = new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true); this.encoder.registerObjectMappersForType(JacksonViewBean.class, map -> map.put(halMediaType, mapper)); + String ls = System.lineSeparator(); // output below is different between Unix and Windows testEncode(Mono.just(jacksonValue), type, halMediaType, Collections.emptyMap(), step -> step - .consumeNextWith(expectString("{\n \"withView1\" : \"with\"\n}").andThen(DataBufferUtils::release)) + .consumeNextWith(expectString("{" + ls + " \"withView1\" : \"with\"" + ls + "}") + .andThen(DataBufferUtils::release)) .verifyComplete() ); } - @Test // gh-22771 + @Test // gh-22771 public void encodeWithFlushAfterWriteOff() { ObjectMapper mapper = new ObjectMapper(); mapper.configure(SerializationFeature.FLUSH_AFTER_WRITE_VALUE, false); From 10e979e58b66ace7241c013e1684251d3d325d81 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 14:37:41 +0200 Subject: [PATCH 16/64] Polishing --- .../jmx/export/notificationPublisherTests.xml | 2 +- .../http/server/reactive/ServletServerHttpRequest.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml index 9e7c75135c21..5f4b476586b6 100644 --- a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml +++ b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 7183234e1d4a..51fa59839afb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -198,7 +198,7 @@ public InetSocketAddress getRemoteAddress() { @Nullable protected SslInfo initSslInfo() { X509Certificate[] certificates = getX509Certificates(); - return certificates != null ? new DefaultSslInfo(getSslSessionId(), certificates) : null; + return (certificates != null ? new DefaultSslInfo(getSslSessionId(), certificates) : null); } @Nullable @@ -208,8 +208,7 @@ private String getSslSessionId() { @Nullable private X509Certificate[] getX509Certificates() { - String name = "javax.servlet.request.X509Certificate"; - return (X509Certificate[]) this.request.getAttribute(name); + return (X509Certificate[]) this.request.getAttribute("javax.servlet.request.X509Certificate"); } @Override From 35de7e19ee2874d53b9078f6fed14c08c634f1ff Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Apr 2022 14:39:36 +0200 Subject: [PATCH 17/64] Introduce initializer callback for Bean Validation Configuration Closes gh-27956 --- .../ValidatorFactoryTests.java | 118 +++++++++++++----- .../LocalValidatorFactoryBean.java | 21 +++- .../beanvalidation/ValidatorFactoryTests.java | 41 +++++- 3 files changed, 146 insertions(+), 34 deletions(-) diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java index a383ae5f8bf7..ea2fb76ae882 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author 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 javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; @@ -43,6 +44,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; @@ -52,18 +54,18 @@ import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; import static org.assertj.core.api.Assertions.assertThat; /** * @author Juergen Hoeller */ -@SuppressWarnings("resource") -public class ValidatorFactoryTests { +class ValidatorFactoryTests { @Test - @SuppressWarnings("cast") - public void testSimpleValidation() { + void simpleValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -78,15 +80,15 @@ public void testSimpleValidation() { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - @SuppressWarnings("cast") - public void testSimpleValidationWithCustomProvider() { + void simpleValidationWithCustomProvider() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setProviderClass(HibernateValidator.class); validator.afterPropertiesSet(); @@ -102,14 +104,15 @@ public void testSimpleValidationWithCustomProvider() { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - public void testSimpleValidationWithClassLevel() { + void simpleValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -122,10 +125,13 @@ public void testSimpleValidationWithClassLevel() { ConstraintViolation cv = iterator.next(); assertThat(cv.getPropertyPath().toString()).isEqualTo(""); assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationFieldType() { + void springValidationFieldType() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -135,11 +141,16 @@ public void testSpringValidationFieldType() { BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); validator.validate(person, errors); assertThat(errors.getErrorCount()).isEqualTo(1); - assertThat(errors.getFieldError("address").getRejectedValue()).isInstanceOf(ValidAddress.class); + assertThat(errors.getFieldError("address").getRejectedValue()) + .as("Field/Value type mismatch") + .isInstanceOf(ValidAddress.class); + + validator.destroy(); } @Test - public void testSpringValidation() { + void springValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -164,10 +175,13 @@ public void testSpringValidation() { assertThat(errorCodes.contains("NotNull.street")).isTrue(); assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); assertThat(errorCodes.contains("NotNull")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithClassLevel() { + void springValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -182,10 +196,12 @@ public void testSpringValidationWithClassLevel() { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithAutowiredValidator() { + void springValidationWithAutowiredValidator() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( LocalValidatorFactoryBean.class); LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); @@ -202,11 +218,14 @@ public void testSpringValidationWithAutowiredValidator() { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); ctx.close(); } @Test - public void testSpringValidationWithErrorInListElement() { + void springValidationWithErrorInListElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -221,10 +240,13 @@ public void testSpringValidationWithErrorInListElement() { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressList[0].street"); assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); + + validator.destroy(); } @Test - public void testSpringValidationWithErrorInSetElement() { + void springValidationWithErrorInSetElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -239,10 +261,13 @@ public void testSpringValidationWithErrorInSetElement() { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressSet[].street"); assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); + + validator.destroy(); } @Test - public void testInnerBeanValidation() { + void innerBeanValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -251,10 +276,13 @@ public void testInnerBeanValidation() { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testValidationWithOptionalField() { + void validationWithOptionalField() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -263,10 +291,13 @@ public void testValidationWithOptionalField() { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testListValidation() { + void listValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -282,6 +313,34 @@ public void testListValidation() { assertThat(fieldError).isNotNull(); assertThat(fieldError.getRejectedValue()).isEqualTo("X"); assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); + + validator.destroy(); + } + + @Test + void withConstraintValidatorFactory() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConstraintValidatorFactory(cvf); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + + @Test + void withCustomInitializer() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConfigurationInitializer(configuration -> configuration.constraintValidatorFactory(cvf)); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); } @@ -380,8 +439,8 @@ public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { } boolean valid = (value.name == null || !value.address.street.contains(value.name)); if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); } return valid; } @@ -417,6 +476,7 @@ public static class InnerBean { public String getValue() { return value; } + public void setValue(String value) { this.value = value; } @@ -425,8 +485,8 @@ public void setValue(String value) { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) - public static @interface InnerValid { + @Constraint(validatedBy = InnerValidator.class) + public @interface InnerValid { String message() default "NOT VALID"; @@ -446,7 +506,8 @@ public void initialize(InnerValid constraintAnnotation) { public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + context.buildConstraintViolationWithTemplate("NULL") + .addPropertyNode("value").addConstraintViolation(); return false; } return true; @@ -494,7 +555,8 @@ public boolean isValid(List list, ConstraintValidatorContext context) { boolean valid = true; for (int i = 0; i < list.size(); i++) { if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addBeanNode().inIterable().atIndex(i).addConstraintViolation(); valid = false; } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java index 73ec646bead7..25fc0727474a 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.function.Consumer; import javax.validation.Configuration; import javax.validation.ConstraintValidatorFactory; @@ -113,6 +114,9 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter private final Map validationPropertyMap = new HashMap<>(); + @Nullable + private Consumer> configurationInitializer; + @Nullable private ApplicationContext applicationContext; @@ -234,6 +238,18 @@ public Map getValidationPropertyMap() { return this.validationPropertyMap; } + /** + * Specify a callback for customizing the Bean Validation {@code Configuration} instance, + * as an alternative to overriding the {@link #postProcessConfiguration(Configuration)} + * method in custom {@code LocalValidatorFactoryBean} subclasses. + *

This enables convenient customizations for application purposes. Infrastructure + * extensions may keep overriding the {@link #postProcessConfiguration} template method. + * @since 5.3.19 + */ + public void setConfigurationInitializer(Consumer> configurationInitializer) { + this.configurationInitializer = configurationInitializer; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -312,6 +328,9 @@ public void afterPropertiesSet() { this.validationPropertyMap.forEach(configuration::addProperty); // Allow for custom post-processing before we actually build the ValidatorFactory. + if (this.configurationInitializer != null) { + this.configurationInitializer.accept(configuration); + } postProcessConfiguration(configuration); try { diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index ca7c256cd890..4b81bfe12ac9 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -31,6 +31,7 @@ import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; @@ -43,6 +44,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; @@ -313,6 +315,32 @@ void listValidation() { validator.destroy(); } + @Test + void withConstraintValidatorFactory() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConstraintValidatorFactory(cvf); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + + @Test + void withCustomInitializer() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConfigurationInitializer(configuration -> configuration.constraintValidatorFactory(cvf)); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + @NameAddressValid public static class ValidPerson { @@ -409,8 +437,8 @@ public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { } boolean valid = (value.name == null || !value.address.street.contains(value.name)); if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); } return valid; } @@ -446,6 +474,7 @@ public static class InnerBean { public String getValue() { return value; } + public void setValue(String value) { this.value = value; } @@ -454,7 +483,7 @@ public void setValue(String value) { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) + @Constraint(validatedBy = InnerValidator.class) public @interface InnerValid { String message() default "NOT VALID"; @@ -475,7 +504,8 @@ public void initialize(InnerValid constraintAnnotation) { public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + context.buildConstraintViolationWithTemplate("NULL") + .addPropertyNode("value").addConstraintViolation(); return false; } return true; @@ -523,7 +553,8 @@ public boolean isValid(List list, ConstraintValidatorContext context) { boolean valid = true; for (int i = 0; i < list.size(); i++) { if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addBeanNode().inIterable().atIndex(i).addConstraintViolation(); valid = false; } } From 5f6d8df34b00811cbd6723b51815923b80b7a4c6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 9 Apr 2022 09:35:51 +0200 Subject: [PATCH 18/64] Introduce isLambdaClass() as a public utility in ClassUtils This commit extracts isLambda() from AopProxyUtils and makes it publicly available as ClassUtils.isLambdaClass(). This is a prerequisite for gh-28209. --- .../aop/framework/AopProxyUtils.java | 16 +---- .../aop/framework/DefaultAopProxyFactory.java | 3 +- .../aop/framework/AopProxyUtilsTests.java | 58 ------------------ .../org/springframework/util/ClassUtils.java | 14 +++++ .../springframework/util/ClassUtilsTests.java | 60 ++++++++++++++++++- 5 files changed, 76 insertions(+), 75 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index f6f7bd7acfd6..08274272386a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -134,7 +134,7 @@ static Class[] completeProxiedInterfaces(AdvisedSupport advised, boolean deco if (targetClass.isInterface()) { advised.setInterfaces(targetClass); } - else if (Proxy.isProxyClass(targetClass) || isLambda(targetClass)) { + else if (Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { advised.setInterfaces(targetClass.getInterfaces()); } specifiedInterfaces = advised.getProxiedInterfaces(); @@ -245,18 +245,4 @@ static Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] argu return arguments; } - /** - * Determine if the supplied {@link Class} is a JVM-generated implementation - * class for a lambda expression or method reference. - *

This method makes a best-effort attempt at determining this, based on - * checks that work on modern, main stream JVMs. - * @param clazz the class to check - * @return {@code true} if the class is a lambda implementation class - * @since 5.3.16 - */ - static boolean isLambda(Class clazz) { - return (clazz.isSynthetic() && (clazz.getSuperclass() == Object.class) && - (clazz.getInterfaces().length > 0) && clazz.getName().contains("$$Lambda")); - } - } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java index 5f1acad9a9a2..e63e17212322 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java @@ -21,6 +21,7 @@ import org.springframework.aop.SpringProxy; import org.springframework.core.NativeDetector; +import org.springframework.util.ClassUtils; /** * Default {@link AopProxyFactory} implementation, creating either a CGLIB proxy @@ -60,7 +61,7 @@ public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } - if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || AopProxyUtils.isLambda(targetClass)) { + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java index 3dbf550a1211..2704cf1c7c2f 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -19,7 +19,6 @@ import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.List; -import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -134,61 +133,4 @@ public void testProxiedUserInterfacesWithNoInterface() { AopProxyUtils.proxiedUserInterfaces(proxy)); } - @Test - void isLambda() { - assertIsLambda(AopProxyUtilsTests.staticLambdaExpression); - assertIsLambda(AopProxyUtilsTests::staticStringFactory); - - assertIsLambda(this.instanceLambdaExpression); - assertIsLambda(this::instanceStringFactory); - } - - @Test - void isNotLambda() { - assertIsNotLambda(new EnigmaSupplier()); - - assertIsNotLambda(new Supplier() { - @Override - public String get() { - return "anonymous inner class"; - } - }); - - assertIsNotLambda(new Fake$$LambdaSupplier()); - } - - private static void assertIsLambda(Supplier supplier) { - assertThat(AopProxyUtils.isLambda(supplier.getClass())).isTrue(); - } - - private static void assertIsNotLambda(Supplier supplier) { - assertThat(AopProxyUtils.isLambda(supplier.getClass())).isFalse(); - } - - private static final Supplier staticLambdaExpression = () -> "static lambda expression"; - - private final Supplier instanceLambdaExpression = () -> "instance lambda expressions"; - - private static String staticStringFactory() { - return "static string factory"; - } - - private String instanceStringFactory() { - return "instance string factory"; - } - - private static class EnigmaSupplier implements Supplier { - @Override - public String get() { - return "enigma"; - } - } - - private static class Fake$$LambdaSupplier implements Supplier { - @Override - public String get() { - return "fake lambda"; - } - } - } diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index 0df6e0ece4c1..d5858c7399c7 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -842,6 +842,20 @@ public static boolean isInnerClass(Class clazz) { return (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())); } + /** + * Determine if the supplied {@link Class} is a JVM-generated implementation + * class for a lambda expression or method reference. + *

This method makes a best-effort attempt at determining this, based on + * checks that work on modern, mainstream JVMs. + * @param clazz the class to check + * @return {@code true} if the class is a lambda implementation class + * @since 5.3.19 + */ + public static boolean isLambdaClass(Class clazz) { + return (clazz.isSynthetic() && (clazz.getSuperclass() == Object.class) && + (clazz.getInterfaces().length > 0) && clazz.getName().contains("$$Lambda")); + } + /** * Check whether the given object is a CGLIB proxy. * @param object the object to check diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java index f14412ba4b18..640ec87eb5b4 100644 --- a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author 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 java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -408,6 +409,29 @@ void isPrimitiveOrWrapperWithWrapper(Class type) { assertThat(ClassUtils.isPrimitiveOrWrapper(type)).isTrue(); } + @Test + void isLambda() { + assertIsLambda(ClassUtilsTests.staticLambdaExpression); + assertIsLambda(ClassUtilsTests::staticStringFactory); + + assertIsLambda(this.instanceLambdaExpression); + assertIsLambda(this::instanceStringFactory); + } + + @Test + void isNotLambda() { + assertIsNotLambda(new EnigmaSupplier()); + + assertIsNotLambda(new Supplier() { + @Override + public String get() { + return "anonymous inner class"; + } + }); + + assertIsNotLambda(new Fake$$LambdaSupplier()); + } + @Nested class GetStaticMethodTests { @@ -500,4 +524,38 @@ void print(String header, String[] messages, String footer) { } } + private static void assertIsLambda(Supplier supplier) { + assertThat(ClassUtils.isLambdaClass(supplier.getClass())).isTrue(); + } + + private static void assertIsNotLambda(Supplier supplier) { + assertThat(ClassUtils.isLambdaClass(supplier.getClass())).isFalse(); + } + + private static final Supplier staticLambdaExpression = () -> "static lambda expression"; + + private final Supplier instanceLambdaExpression = () -> "instance lambda expressions"; + + private static String staticStringFactory() { + return "static string factory"; + } + + private String instanceStringFactory() { + return "instance string factory"; + } + + private static class EnigmaSupplier implements Supplier { + @Override + public String get() { + return "enigma"; + } + } + + private static class Fake$$LambdaSupplier implements Supplier { + @Override + public String get() { + return "fake lambda"; + } + } + } From 6fad00ed222c48f9d845bcea9d5a50dcf7c2a169 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 9 Apr 2022 09:57:34 +0200 Subject: [PATCH 19/64] Ensure dynamic proxy with AOP introduction includes lambda interfaces Closes gh-28209 --- .../autoproxy/AbstractAutoProxyCreator.java | 8 +- .../AspectJAutoProxyCreatorTests.java | 105 +++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index 3e68f820ecb9..c550168800e4 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author 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,6 +50,7 @@ import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -85,6 +86,7 @@ * @author Juergen Hoeller * @author Rod Johnson * @author Rob Harrop + * @author Sam Brannen * @since 13.10.2003 * @see #setInterceptorNames * @see #getAdvicesAndAdvisorsForBean @@ -442,8 +444,8 @@ protected Object createProxy(Class beanClass, @Nullable String beanName, proxyFactory.copyFrom(this); if (proxyFactory.isProxyTargetClass()) { - // Explicit handling of JDK proxy targets (for introduction advice scenarios) - if (Proxy.isProxyClass(beanClass)) { + // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) + if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) { // Must allow for introductions; can't just set interfaces to the proxy's interfaces only. for (Class ifc : beanClass.getInterfaces()) { proxyFactory.addInterface(ifc); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java index 7c017cfa1aad..c506e210636f 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java @@ -21,6 +21,8 @@ import java.lang.reflect.Method; import java.util.function.Supplier; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -31,11 +33,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInterceptor; import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.SpringProxy; import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; import org.springframework.aop.aspectj.annotation.AspectMetadata; import org.springframework.aop.config.AopConfigUtils; +import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyConfig; +import org.springframework.aop.support.AbstractPointcutAdvisor; import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; import org.springframework.beans.PropertyValue; @@ -52,6 +60,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.DecoratingProxy; import org.springframework.core.NestedRuntimeException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -304,10 +313,26 @@ public void testWithBeanNameAutoProxyCreator() { @ValueSource(classes = {ProxyTargetClassFalseConfig.class, ProxyTargetClassTrueConfig.class}) void lambdaIsAlwaysProxiedWithJdkProxy(Class configClass) { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { - Supplier supplier = context.getBean(Supplier.class); + @SuppressWarnings("unchecked") + Supplier supplier = context.getBean(Supplier.class); assertThat(AopUtils.isAopProxy(supplier)).as("AOP proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(supplier)).as("JDK Dynamic proxy").isTrue(); - assertThat(supplier.get()).asString().isEqualTo("advised: lambda"); + assertThat(supplier.getClass().getInterfaces()) + .containsExactlyInAnyOrder(Supplier.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(supplier.get()).isEqualTo("advised: lambda"); + } + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(classes = {MixinProxyTargetClassFalseConfig.class, MixinProxyTargetClassTrueConfig.class}) + void lambdaIsAlwaysProxiedWithJdkProxyWithIntroductions(Class configClass) { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { + MessageGenerator messageGenerator = context.getBean(MessageGenerator.class); + assertThat(AopUtils.isAopProxy(messageGenerator)).as("AOP proxy").isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(messageGenerator)).as("JDK Dynamic proxy").isTrue(); + assertThat(messageGenerator.getClass().getInterfaces()) + .containsExactlyInAnyOrder(MessageGenerator.class, Mixin.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(messageGenerator.generateMessage()).isEqualTo("mixin: lambda"); } } @@ -616,3 +641,79 @@ class ProxyTargetClassFalseConfig extends AbstractProxyTargetClassConfig { @EnableAspectJAutoProxy(proxyTargetClass = true) class ProxyTargetClassTrueConfig extends AbstractProxyTargetClassConfig { } + +@FunctionalInterface +interface MessageGenerator { + String generateMessage(); +} + +interface Mixin { +} + +class MixinIntroductionInterceptor implements IntroductionInterceptor { + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + return "mixin: " + invocation.proceed(); + } + + @Override + public boolean implementsInterface(Class intf) { + return Mixin.class.isAssignableFrom(intf); + } + +} + +@SuppressWarnings("serial") +class MixinAdvisor extends AbstractPointcutAdvisor implements IntroductionAdvisor { + + @Override + public org.springframework.aop.Pointcut getPointcut() { + return org.springframework.aop.Pointcut.TRUE; + } + + @Override + public Advice getAdvice() { + return new MixinIntroductionInterceptor(); + } + + @Override + public Class[] getInterfaces() { + return new Class[] { Mixin.class }; + } + + @Override + public ClassFilter getClassFilter() { + return MessageGenerator.class::isAssignableFrom; + } + + @Override + public void validateInterfaces() { + /* no-op */ + } + +} + +abstract class AbstractMixinConfig { + + @Bean + MessageGenerator messageGenerator() { + return () -> "lambda"; + } + + @Bean + MixinAdvisor mixinAdvisor() { + return new MixinAdvisor(); + } + +} + +@Configuration(proxyBeanMethods = false) +@EnableAspectJAutoProxy(proxyTargetClass = false) +class MixinProxyTargetClassFalseConfig extends AbstractMixinConfig { +} + +@Configuration(proxyBeanMethods = false) +@EnableAspectJAutoProxy(proxyTargetClass = true) +class MixinProxyTargetClassTrueConfig extends AbstractMixinConfig { +} From 8b396985534268c59509f60a1f7d50c47a7d2bbd Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Apr 2022 15:41:58 +0200 Subject: [PATCH 20/64] Upgrade to Reactor 2020.0.18 Closes gh-28329 --- build.gradle | 2 +- .../org/springframework/core/io/buffer/DataBufferUtils.java | 6 +++--- .../http/codec/multipart/MultipartParser.java | 4 ++-- .../springframework/http/codec/multipart/PartGenerator.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 98f15755263a..dc4cc1da1dcb 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6" mavenBom "io.netty:netty-bom:4.1.75.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.17" + mavenBom "io.projectreactor:reactor-bom:2020.0.18" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" mavenBom "io.rsocket:rsocket-bom:1.1.2" mavenBom "org.eclipse.jetty:jetty-bom:9.4.46.v20220331" diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index ad0419ee4b77..3e9668875661 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1061,7 +1061,7 @@ protected void hookOnComplete() { @Override public Context currentContext() { - return this.sink.currentContext(); + return Context.of(this.sink.contextView()); } } @@ -1158,7 +1158,7 @@ private void sinkDataBuffer() { @Override public Context currentContext() { - return this.sink.currentContext(); + return Context.of(this.sink.contextView()); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index ff1344424aa6..7ef9e9aa9e44 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,7 +105,7 @@ public static Flux parse(Flux buffers, byte[] boundary, int m @Override public Context currentContext() { - return this.sink.currentContext(); + return Context.of(this.sink.contextView()); } @Override 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 88d689d90e9b..484a09c6adba 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,7 +116,7 @@ public static Flux createParts(Flux tokens, int max @Override public Context currentContext() { - return this.sink.currentContext(); + return Context.of(this.sink.contextView()); } @Override From 3b4ae7b028799e6b9f33e44ead1d9038d0d55b47 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Apr 2022 16:52:13 +0200 Subject: [PATCH 21/64] TomcatHttpHandlerAdapter continues after 0 bytes This commit makes sure that TomcatServerHttpRequest::readFromInputStream follows the same contract as the method it overrides, and returns AbstractListenerReadPublisher.EMPTY_BUFFER when 0 bytes are read. See gh-28241 --- .../http/server/reactive/TomcatHttpHandlerAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index 7920c7ffd8b4..b8c78fdbf4e4 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -153,7 +153,7 @@ else if (read == -1) { return EOF_BUFFER; } else { - return null; + return AbstractListenerReadPublisher.EMPTY_BUFFER; } } finally { From 949c3d450c35b676b82a4a56ada997cf9a552f1d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Apr 2022 00:24:06 +0200 Subject: [PATCH 22/64] Align plain accessor check --- .../beans/CachedIntrospectionResults.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 2929509d2343..8332045197de 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -367,7 +367,7 @@ private void introspectPlainAccessors(Class beanClass, Set readMethod for (Method method : beanClass.getMethods()) { if (!this.propertyDescriptors.containsKey(method.getName()) && - !readMethodNames.contains((method.getName())) && isPlainAccessor(method)) { + !readMethodNames.contains(method.getName()) && isPlainAccessor(method)) { this.propertyDescriptors.put(method.getName(), new GenericTypeAwarePropertyDescriptor(beanClass, method.getName(), method, null, null)); readMethodNames.add(method.getName()); @@ -376,8 +376,11 @@ private void introspectPlainAccessors(Class beanClass, Set readMethod } private boolean isPlainAccessor(Method method) { - if (method.getParameterCount() > 0 || method.getReturnType() == void.class || - method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) { + if (Modifier.isStatic(method.getModifiers()) || + method.getDeclaringClass() == Object.class || method.getDeclaringClass() == Class.class || + method.getParameterCount() > 0 || method.getReturnType() == void.class || + ClassLoader.class.isAssignableFrom(method.getReturnType()) || + ProtectionDomain.class.isAssignableFrom(method.getReturnType())) { return false; } try { From 0cf7f7bd890504061883c48e382dc23e0c5d4df0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Apr 2022 00:24:23 +0200 Subject: [PATCH 23/64] Polishing --- .../ModelAttributeMethodProcessorTests.java | 15 +++++++-------- ...letAnnotationControllerHandlerMethodTests.java | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) 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 bc3be0e7aa99..c3ab1ed07256 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-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -269,7 +269,7 @@ public void handleNotAnnotatedReturnValue() throws Exception { assertThat(this.container.getModel().get("testBean")).isSameAs(testBean); } - @Test // gh-25182 + @Test // gh-25182 public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() throws Exception { MockHttpServletRequest mockRequest = new MockHttpServletRequest(); mockRequest.addParameter("listOfStrings", "1,2"); @@ -279,7 +279,6 @@ public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() t 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; @@ -309,7 +308,6 @@ private static class StubRequestDataBinder extends WebRequestDataBinder { private boolean validateInvoked; - public StubRequestDataBinder(Object target, String objectName) { super(target, objectName); } @@ -345,7 +343,7 @@ public void validate(Object... validationHints) { } - @SessionAttributes(types=TestBean.class) + @SessionAttributes(types = TestBean.class) private static class ModelAttributeHandler { @SuppressWarnings("unused") @@ -360,6 +358,7 @@ public void modelAttribute( } } + static class TestBeanWithConstructorArgs { final List listOfStrings; @@ -367,15 +366,15 @@ static class TestBeanWithConstructorArgs { public TestBeanWithConstructorArgs(List listOfStrings) { this.listOfStrings = listOfStrings; } - } - @ModelAttribute("modelAttrName") @SuppressWarnings("unused") + + @ModelAttribute("modelAttrName") + @SuppressWarnings("unused") private String annotatedReturnValue() { return null; } - @SuppressWarnings("unused") private TestBean notAnnotatedReturnValue() { return null; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index aeaf049adb23..2b9d7125d7d7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2236,6 +2236,7 @@ void routerFunction() throws ServletException, IOException { assertThat(response.getContentAsString()).isEqualTo("foo-body"); } + @Controller static class ControllerWithEmptyValueMapping { @@ -3573,7 +3574,6 @@ public void httpHeaders(@RequestHeader HttpHeaders headers, Writer writer) throw assertThat(headers.getContentType()).as("Invalid Content-Type").isEqualTo(new MediaType("text", "html")); multiValueMap(headers, writer); } - } @Controller From a7cf19cec5ebd270f97a194d749e2d5701ad2ab7 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 3 Apr 2022 16:19:52 +0200 Subject: [PATCH 24/64] Improve documentation and matching algorithm in data binders --- .../beans/PropertyAccessor.java | 7 +- .../validation/DataBinder.java | 117 +++++--- .../validation/DataBinderTests.java | 262 +++++++++--------- .../web/bind/ServletRequestDataBinder.java | 11 +- .../web/bind/WebDataBinder.java | 11 +- .../web/bind/annotation/ExceptionHandler.java | 1 + .../web/bind/annotation/InitBinder.java | 22 +- .../web/bind/annotation/ModelAttribute.java | 26 +- .../bind/support/WebExchangeDataBinder.java | 11 +- .../bind/support/WebRequestDataBinder.java | 11 +- .../InitBinderDataBinderFactoryTests.java | 4 +- .../InitBinderBindingContextTests.java | 4 +- .../ExtendedServletRequestDataBinder.java | 11 +- .../web/web-data-binding-model-design.adoc | 95 +++++++ src/docs/asciidoc/web/webflux.adoc | 5 + src/docs/asciidoc/web/webmvc.adoc | 7 + 16 files changed, 402 insertions(+), 203 deletions(-) create mode 100644 src/docs/asciidoc/web/web-data-binding-model-design.adoc diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java index 3a417aadcd37..03201a89d0d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,9 @@ /** * Common interface for classes that can access named properties - * (such as bean properties of an object or fields in an object) - * Serves as base interface for {@link BeanWrapper}. + * (such as bean properties of an object or fields in an object). + * + *

Serves as base interface for {@link BeanWrapper}. * * @author Juergen Hoeller * @since 1.1 diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 612dfc5622a2..8ee5e43b02fe 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,18 +51,20 @@ import org.springframework.util.StringUtils; /** - * Binder that allows for setting property values onto a target object, - * including support for validation and binding result analysis. - * The binding process can be customized through specifying allowed fields, + * Binder that allows for setting property values on a target object, including + * support for validation and binding result analysis. + * + *

The binding process can be customized by specifying allowed field patterns, * required fields, custom editors, etc. * - *

Note that there are potential security implications in failing to set an array - * of allowed fields. In the case of HTTP form POST data for example, malicious clients - * can attempt to subvert an application by supplying values for fields or properties - * that do not exist on the form. In some cases this could lead to illegal data being - * set on command objects or their nested objects. For this reason, it is - * highly recommended to specify the {@link #setAllowedFields allowedFields} property - * on the DataBinder. + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. * *

The binding results can be examined via the {@link BindingResult} interface, * extending the {@link Errors} interface: see the {@link #getBindingResult()} method. @@ -96,6 +98,7 @@ * @author Rob Harrop * @author Stephane Nicoll * @author Kazuki Shimizu + * @author Sam Brannen * @see #setAllowedFields * @see #setRequiredFields * @see #registerCustomEditor @@ -418,15 +421,21 @@ public boolean isIgnoreInvalidFields() { } /** - * Register fields that should be allowed for binding. Default is all fields. - * Restrict this for example to avoid unwanted modifications by malicious + * Register field patterns that should be allowed for binding. + *

Default is all fields. + *

Restrict this for example to avoid unwanted modifications by malicious * users when binding HTTP request parameters. - *

Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an - * arbitrary number of pattern parts), as well as direct equality. More - * sophisticated matching can be implemented by overriding the - * {@code isAllowed} method. - *

Alternatively, specify a list of disallowed fields. - * @param allowedFields array of field names + *

Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and + * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as + * well as direct equality. + *

The default implementation of this method stores allowed field patterns + * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical} + * form. Subclasses which override this method must therefore take this into + * account. + *

More sophisticated matching can be implemented by overriding the + * {@link #isAllowed} method. + *

Alternatively, specify a list of disallowed field patterns. + * @param allowedFields array of allowed field patterns * @see #setDisallowedFields * @see #isAllowed(String) */ @@ -435,8 +444,9 @@ public void setAllowedFields(@Nullable String... allowedFields) { } /** - * Return the fields that should be allowed for binding. - * @return array of field names + * Return the field patterns that should be allowed for binding. + * @return array of allowed field patterns + * @see #setAllowedFields(String...) */ @Nullable public String[] getAllowedFields() { @@ -444,25 +454,44 @@ public String[] getAllowedFields() { } /** - * Register fields that should not be allowed for binding. Default - * is none. Mark fields as disallowed for example to avoid unwanted + * Register field patterns that should not be allowed for binding. + *

Default is none. + *

Mark fields as disallowed, for example to avoid unwanted * modifications by malicious users when binding HTTP request parameters. - *

Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an - * arbitrary number of pattern parts), as well as direct equality. - * More sophisticated matching can be implemented by overriding the - * {@code isAllowed} method. - *

Alternatively, specify a list of allowed fields. - * @param disallowedFields array of field names + *

Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and + * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as + * well as direct equality. + *

The default implementation of this method stores disallowed field patterns + * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical} + * form. As of Spring Framework 5.2.21, the default implementation also transforms + * disallowed field patterns to {@linkplain String#toLowerCase() lowercase} to + * support case-insensitive pattern matching in {@link #isAllowed}. Subclasses + * which override this method must therefore take both of these transformations + * into account. + *

More sophisticated matching can be implemented by overriding the + * {@link #isAllowed} method. + *

Alternatively, specify a list of allowed field patterns. + * @param disallowedFields array of disallowed field patterns * @see #setAllowedFields * @see #isAllowed(String) */ public void setDisallowedFields(@Nullable String... disallowedFields) { - this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields); + if (disallowedFields == null) { + this.disallowedFields = null; + } + else { + String[] fieldPatterns = new String[disallowedFields.length]; + for (int i = 0; i < fieldPatterns.length; i++) { + fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]).toLowerCase(); + } + this.disallowedFields = fieldPatterns; + } } /** - * Return the fields that should not be allowed for binding. - * @return array of field names + * Return the field patterns that should not be allowed for binding. + * @return array of disallowed field patterns + * @see #setDisallowedFields(String...) */ @Nullable public String[] getDisallowedFields() { @@ -774,16 +803,20 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) { } /** - * Return if the given field is allowed for binding. - * Invoked for each passed-in property value. - *

The default implementation checks for "xxx*", "*xxx", "*xxx*" and "xxx*yyy" - * matches (with an arbitrary number of pattern parts), as well as direct equality, - * in the specified lists of allowed fields and disallowed fields. A field matching - * a disallowed pattern will not be accepted even if it also happens to match a - * pattern in the allowed list. - *

Can be overridden in subclasses. + * Determine if the given field is allowed for binding. + *

Invoked for each passed-in property value. + *

Checks for {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and + * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as + * well as direct equality, in the configured lists of allowed field patterns + * and disallowed field patterns. + *

Matching against allowed field patterns is case-sensitive; whereas, + * matching against disallowed field patterns is case-insensitive. + *

A field matching a disallowed pattern will not be accepted even if it + * also happens to match a pattern in the allowed list. + *

Can be overridden in subclasses, but care must be taken to honor the + * aforementioned contract. * @param field the field to check - * @return if the field is allowed + * @return {@code true} if the field is allowed * @see #setAllowedFields * @see #setDisallowedFields * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) @@ -792,7 +825,7 @@ protected boolean isAllowed(String field) { String[] allowed = getAllowedFields(); String[] disallowed = getDisallowedFields(); return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) && - (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field))); + (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase()))); } /** diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 3d39e2ecd309..546c599c01f7 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -64,23 +64,26 @@ import org.springframework.format.support.FormattingConversionService; import org.springframework.lang.Nullable; import org.springframework.tests.sample.beans.BeanWithObjectProperty; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; /** + * Unit tests for {@link DataBinder}. + * * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop * @author Kazuki Shimizu + * @author Sam Brannen */ class DataBinderTests { @Test - void testBindingNoErrors() throws BindException { + void bindingNoErrors() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); assertThat(binder.isIgnoreUnknownFields()).isTrue(); @@ -110,12 +113,11 @@ void testBindingNoErrors() throws BindException { assertThat(ex).isEqualTo(binder.getBindingResult()); other.reject("xxx"); - boolean condition = !other.equals(binder.getBindingResult()); - assertThat(condition).isTrue(); + assertThat(other).isNotEqualTo(binder.getBindingResult()); } @Test - void testBindingWithDefaultConversionNoErrors() throws BindException { + void bindingWithDefaultConversionNoErrors() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); assertThat(binder.isIgnoreUnknownFields()).isTrue(); @@ -131,7 +133,7 @@ void testBindingWithDefaultConversionNoErrors() throws BindException { } @Test - void testNestedBindingWithDefaultConversionNoErrors() throws BindException { + void nestedBindingWithDefaultConversionNoErrors() throws BindException { TestBean rod = new TestBean(new TestBean()); DataBinder binder = new DataBinder(rod, "person"); assertThat(binder.isIgnoreUnknownFields()).isTrue(); @@ -147,7 +149,7 @@ void testNestedBindingWithDefaultConversionNoErrors() throws BindException { } @Test - void testBindingNoErrorsNotIgnoreUnknown() { + void bindingNoErrorsNotIgnoreUnknown() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setIgnoreUnknownFields(false); @@ -160,7 +162,7 @@ void testBindingNoErrorsNotIgnoreUnknown() { } @Test - void testBindingNoErrorsWithInvalidField() { + void bindingNoErrorsWithInvalidField() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -171,7 +173,7 @@ void testBindingNoErrorsWithInvalidField() { } @Test - void testBindingNoErrorsWithIgnoreInvalid() { + void bindingNoErrorsWithIgnoreInvalid() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setIgnoreInvalidFields(true); @@ -180,10 +182,14 @@ void testBindingNoErrorsWithIgnoreInvalid() { pvs.add("spouse.age", 32); binder.bind(pvs); + binder.close(); + + assertThat(rod.getName()).isEqualTo("Rod"); + assertThat(rod.getSpouse()).isNull(); } @Test - void testBindingWithErrors() { + void bindingWithErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -245,7 +251,7 @@ void testBindingWithErrors() { } @Test - void testBindingWithSystemFieldError() { + void bindingWithSystemFieldError() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -257,7 +263,7 @@ void testBindingWithSystemFieldError() { } @Test - void testBindingWithErrorsAndCustomEditors() { + void bindingWithErrorsAndCustomEditors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { @@ -325,7 +331,7 @@ public String getAsText() { } @Test - void testBindingWithCustomEditorOnObjectField() { + void bindingWithCustomEditorOnObjectField() { BeanWithObjectProperty tb = new BeanWithObjectProperty(); DataBinder binder = new DataBinder(tb); binder.registerCustomEditor(Integer.class, "object", new CustomNumberEditor(Integer.class, true)); @@ -336,7 +342,7 @@ void testBindingWithCustomEditorOnObjectField() { } @Test - void testBindingWithFormatter() { + void bindingWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -368,7 +374,7 @@ void testBindingWithFormatter() { } @Test - void testBindingErrorWithFormatter() { + void bindingErrorWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -391,7 +397,7 @@ void testBindingErrorWithFormatter() { } @Test - void testBindingErrorWithParseExceptionFromFormatter() { + void bindingErrorWithParseExceptionFromFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -419,7 +425,7 @@ public String print(String object, Locale locale) { } @Test - void testBindingErrorWithRuntimeExceptionFromFormatter() { + void bindingErrorWithRuntimeExceptionFromFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -447,7 +453,7 @@ public String print(String object, Locale locale) { } @Test - void testBindingWithFormatterAgainstList() { + void bindingWithFormatterAgainstList() { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -469,7 +475,7 @@ void testBindingWithFormatterAgainstList() { } @Test - void testBindingErrorWithFormatterAgainstList() { + void bindingErrorWithFormatterAgainstList() { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -492,7 +498,7 @@ void testBindingErrorWithFormatterAgainstList() { } @Test - void testBindingWithFormatterAgainstFields() { + void bindingWithFormatterAgainstFields() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); @@ -525,7 +531,7 @@ void testBindingWithFormatterAgainstFields() { } @Test - void testBindingErrorWithFormatterAgainstFields() { + void bindingErrorWithFormatterAgainstFields() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); binder.initDirectFieldAccess(); @@ -549,7 +555,7 @@ void testBindingErrorWithFormatterAgainstFields() { } @Test - void testBindingWithCustomFormatter() { + void bindingWithCustomFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); binder.addCustomFormatter(new NumberStyleFormatter(), Float.class); @@ -578,7 +584,7 @@ void testBindingWithCustomFormatter() { } @Test - void testBindingErrorWithCustomFormatter() { + void bindingErrorWithCustomFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); binder.addCustomFormatter(new NumberStyleFormatter()); @@ -599,7 +605,7 @@ void testBindingErrorWithCustomFormatter() { } @Test - void testBindingErrorWithParseExceptionFromCustomFormatter() { + void bindingErrorWithParseExceptionFromCustomFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -624,7 +630,7 @@ public String print(String object, Locale locale) { } @Test - void testBindingErrorWithRuntimeExceptionFromCustomFormatter() { + void bindingErrorWithRuntimeExceptionFromCustomFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -649,7 +655,7 @@ public String print(String object, Locale locale) { } @Test - void testConversionWithInappropriateStringEditor() { + void conversionWithInappropriateStringEditor() { DataBinder dataBinder = new DataBinder(null); DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); dataBinder.setConversionService(conversionService); @@ -662,7 +668,7 @@ void testConversionWithInappropriateStringEditor() { } @Test - void testBindingWithAllowedFields() throws BindException { + void bindingWithAllowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "myparam"); @@ -672,30 +678,32 @@ void testBindingWithAllowedFields() throws BindException { binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isZero(); } @Test - void testBindingWithDisallowedFields() throws BindException { + void bindingWithDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); - binder.setDisallowedFields("age"); + binder.setDisallowedFields(" ", "\t", "favouriteColour", null, "age"); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); + pvs.add("favouriteColour", "BLUE"); binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); - String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); - assertThat(disallowedFields.length).isEqualTo(1); - assertThat(disallowedFields[0]).isEqualTo("age"); + + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isZero(); + assertThat(rod.getFavouriteColour()).as("did not change favourite colour").isNull(); + assertThat(binder.getBindingResult().getSuppressedFields()).containsExactlyInAnyOrder("age", "favouriteColour"); } @Test - void testBindingWithAllowedAndDisallowedFields() throws BindException { + void bindingWithAllowedAndDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "myparam"); @@ -706,34 +714,32 @@ void testBindingWithAllowedAndDisallowedFields() throws BindException { binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); - String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); - assertThat(disallowedFields).hasSize(1); - assertThat(disallowedFields[0]).isEqualTo("age"); + + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isZero(); + assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age"); } @Test - void testBindingWithOverlappingAllowedAndDisallowedFields() throws BindException { + void bindingWithOverlappingAllowedAndDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "age"); - binder.setDisallowedFields("age"); + binder.setDisallowedFields("AGE"); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "Rod"); pvs.add("age", "32x"); binder.bind(pvs); binder.close(); - assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); - assertThat(rod.getAge() == 0).as("did not change age").isTrue(); - String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); - assertThat(disallowedFields).hasSize(1); - assertThat(disallowedFields[0]).isEqualTo("age"); + + assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod"); + assertThat(rod.getAge()).as("did not change age").isZero(); + assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age"); } @Test - void testBindingWithAllowedFieldsUsingAsterisks() throws BindException { + void bindingWithAllowedFieldsUsingAsterisks() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setAllowedFields("nam*", "*ouchy"); @@ -760,11 +766,11 @@ void testBindingWithAllowedFieldsUsingAsterisks() throws BindException { } @Test - void testBindingWithAllowedAndDisallowedMapFields() throws BindException { + void bindingWithAllowedAndDisallowedMapFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("someMap[key1]", "someMap[key2]"); - binder.setDisallowedFields("someMap['key3']", "someMap[key4]"); + binder.setDisallowedFields("someMap['KEY3']", "SomeMap[key4]"); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("someMap[key1]", "value1"); @@ -774,21 +780,18 @@ void testBindingWithAllowedAndDisallowedMapFields() throws BindException { binder.bind(pvs); binder.close(); - assertThat(rod.getSomeMap().get("key1")).isEqualTo("value1"); - assertThat(rod.getSomeMap().get("key2")).isEqualTo("value2"); - assertThat(rod.getSomeMap().get("key3")).isNull(); - assertThat(rod.getSomeMap().get("key4")).isNull(); - String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); - assertThat(disallowedFields).hasSize(2); - assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key3]")).isTrue(); - assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key4]")).isTrue(); + + @SuppressWarnings("unchecked") + Map someMap = (Map) rod.getSomeMap(); + assertThat(someMap).containsOnly(entry("key1", "value1"), entry("key2", "value2")); + assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("someMap[key3]", "someMap[key4]"); } /** * Tests for required field, both null, non-existing and empty strings. */ @Test - void testBindingWithRequiredFields() { + void bindingWithRequiredFields() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -819,7 +822,7 @@ void testBindingWithRequiredFields() { } @Test - void testBindingWithRequiredMapFields() { + void bindingWithRequiredMapFields() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -839,7 +842,7 @@ void testBindingWithRequiredMapFields() { } @Test - void testBindingWithNestedObjectCreation() { + void bindingWithNestedObjectCreation() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "person"); @@ -860,7 +863,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorWithOldValueAccess() { + void customEditorWithOldValueAccess() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -885,7 +888,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testCustomEditorForSingleProperty() { + void customEditorForSingleProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); DataBinder binder = new DataBinder(tb, "tb"); @@ -925,7 +928,7 @@ public String getAsText() { } @Test - void testCustomEditorForPrimitiveProperty() { + void customEditorForPrimitiveProperty() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -949,7 +952,7 @@ public String getAsText() { } @Test - void testCustomEditorForAllStringProperties() { + void customEditorForAllStringProperties() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -981,7 +984,7 @@ public String getAsText() { } @Test - void testCustomFormatterForSingleProperty() { + void customFormatterForSingleProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); DataBinder binder = new DataBinder(tb, "tb"); @@ -1021,7 +1024,7 @@ public String print(String object, Locale locale) { } @Test - void testCustomFormatterForPrimitiveProperty() { + void customFormatterForPrimitiveProperty() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1045,7 +1048,7 @@ public String print(Integer object, Locale locale) { } @Test - void testCustomFormatterForAllStringProperties() { + void customFormatterForAllStringProperties() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1077,7 +1080,7 @@ public String print(String object, Locale locale) { } @Test - void testJavaBeanPropertyConventions() { + void javaBeanPropertyConventions() { Book book = new Book(); DataBinder binder = new DataBinder(book); @@ -1101,7 +1104,7 @@ void testJavaBeanPropertyConventions() { } @Test - void testOptionalProperty() { + void optionalProperty() { OptionalHolder bean = new OptionalHolder(); DataBinder binder = new DataBinder(bean); binder.setConversionService(new DefaultConversionService()); @@ -1122,7 +1125,7 @@ void testOptionalProperty() { } @Test - void testValidatorNoErrors() throws Exception { + void validatorNoErrors() throws Exception { TestBean tb = new TestBean(); tb.setAge(33); tb.setName("Rod"); @@ -1175,15 +1178,13 @@ void testValidatorNoErrors() throws Exception { assertThat(errors.getNestedPath()).isEqualTo("spouse."); assertThat(errors.getErrorCount()).isEqualTo(1); - boolean condition1 = !errors.hasGlobalErrors(); - assertThat(condition1).isTrue(); + assertThat(errors.hasGlobalErrors()).isFalse(); assertThat(errors.getFieldErrorCount("age")).isEqualTo(1); - boolean condition = !errors.hasFieldErrors("name"); - assertThat(condition).isTrue(); + assertThat(errors.hasFieldErrors("name")).isFalse(); } @Test - void testValidatorWithErrors() { + void validatorWithErrors() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -1252,7 +1253,7 @@ void testValidatorWithErrors() { } @Test - void testValidatorWithErrorsAndCodesPrefix() { + void validatorWithErrorsAndCodesPrefix() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -1324,7 +1325,7 @@ void testValidatorWithErrorsAndCodesPrefix() { } @Test - void testValidatorWithNestedObjectNull() { + void validatorWithNestedObjectNull() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); Validator testValidator = new TestBeanValidator(); @@ -1343,7 +1344,7 @@ void testValidatorWithNestedObjectNull() { } @Test - void testNestedValidatorWithoutNestedPath() { + void nestedValidatorWithoutNestedPath() { TestBean tb = new TestBean(); tb.setName("XXX"); Errors errors = new BeanPropertyBindingResult(tb, "tb"); @@ -1357,7 +1358,8 @@ void testNestedValidatorWithoutNestedPath() { } @Test - void testBindingStringArrayToIntegerSet() { + @SuppressWarnings("unchecked") + void bindingStringArrayToIntegerSet() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class) { @@ -1371,12 +1373,8 @@ protected Object convertElement(Object element) { binder.bind(pvs); assertThat(binder.getBindingResult().getFieldValue("set")).isEqualTo(tb.getSet()); - boolean condition = tb.getSet() instanceof TreeSet; - assertThat(condition).isTrue(); - assertThat(tb.getSet().size()).isEqualTo(3); - assertThat(tb.getSet().contains(10)).isTrue(); - assertThat(tb.getSet().contains(20)).isTrue(); - assertThat(tb.getSet().contains(30)).isTrue(); + assertThat(tb.getSet()).isInstanceOf(TreeSet.class); + assertThat((Set) tb.getSet()).containsExactly(10, 20, 30); pvs = new MutablePropertyValues(); pvs.add("set", null); @@ -1386,7 +1384,7 @@ protected Object convertElement(Object element) { } @Test - void testBindingNullToEmptyCollection() { + void bindingNullToEmptyCollection() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class, true)); @@ -1394,13 +1392,12 @@ void testBindingNullToEmptyCollection() { pvs.add("set", null); binder.bind(pvs); - boolean condition = tb.getSet() instanceof TreeSet; - assertThat(condition).isTrue(); - assertThat(tb.getSet().isEmpty()).isTrue(); + assertThat(tb.getSet()).isInstanceOf(TreeSet.class); + assertThat(tb.getSet()).isEmpty(); } @Test - void testBindingToIndexedField() { + void bindingToIndexedField() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { @@ -1439,7 +1436,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testBindingToNestedIndexedField() { + void bindingToNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); @@ -1470,7 +1467,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Test - void testEditorForNestedIndexedField() { + void editorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); @@ -1496,7 +1493,7 @@ public String getAsText() { } @Test - void testSpecificEditorForNestedIndexedField() { + void specificEditorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); @@ -1522,7 +1519,7 @@ public String getAsText() { } @Test - void testInnerSpecificEditorForNestedIndexedField() { + void innerSpecificEditorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); @@ -1548,7 +1545,7 @@ public String getAsText() { } @Test - void testDirectBindingToIndexedField() { + void directBindingToIndexedField() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { @@ -1601,7 +1598,7 @@ public String getAsText() { } @Test - void testDirectBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() { + void directBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(TestBean.class, "map[key0]", new PropertyEditorSupport() { @@ -1632,7 +1629,7 @@ public String getAsText() { } @Test - void testDirectBindingToEmptyIndexedFieldWithRegisteredGenericEditor() { + void directBindingToEmptyIndexedFieldWithRegisteredGenericEditor() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { @@ -1663,7 +1660,7 @@ public String getAsText() { } @Test - void testCustomEditorWithSubclass() { + void customEditorWithSubclass() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { @@ -1697,7 +1694,7 @@ public String getAsText() { } @Test - void testBindToStringArrayWithArrayEditor() { + void bindToStringArrayWithArrayEditor() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(String[].class, "stringArray", new PropertyEditorSupport() { @@ -1709,15 +1706,12 @@ public void setAsText(String text) throws IllegalArgumentException { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("stringArray", "a1-b2"); binder.bind(pvs); - boolean condition = !binder.getBindingResult().hasErrors(); - assertThat(condition).isTrue(); - assertThat(tb.getStringArray().length).isEqualTo(2); - assertThat(tb.getStringArray()[0]).isEqualTo("a1"); - assertThat(tb.getStringArray()[1]).isEqualTo("b2"); + assertThat(binder.getBindingResult().hasErrors()).isFalse(); + assertThat(tb.getStringArray()).containsExactly("a1", "b2"); } @Test - void testBindToStringArrayWithComponentEditor() { + void bindToStringArrayWithComponentEditor() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); binder.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() { @@ -1729,15 +1723,14 @@ public void setAsText(String text) throws IllegalArgumentException { MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("stringArray", new String[] {"a1", "b2"}); binder.bind(pvs); - boolean condition = !binder.getBindingResult().hasErrors(); - assertThat(condition).isTrue(); + assertThat(binder.getBindingResult().hasErrors()).isFalse(); assertThat(tb.getStringArray().length).isEqualTo(2); assertThat(tb.getStringArray()[0]).isEqualTo("Xa1"); assertThat(tb.getStringArray()[1]).isEqualTo("Xb2"); } @Test - void testBindingErrors() { + void bindingErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -1764,7 +1757,7 @@ void testBindingErrors() { } @Test - void testAddAllErrors() { + void addAllErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -1784,7 +1777,7 @@ void testAddAllErrors() { @Test @SuppressWarnings("unchecked") - void testBindingWithResortedList() { + void bindingWithResortedList() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -1802,7 +1795,7 @@ void testBindingWithResortedList() { } @Test - void testRejectWithoutDefaultMessage() { + void rejectWithoutDefaultMessage() { TestBean tb = new TestBean(); tb.setName("myName"); tb.setAge(99); @@ -1820,7 +1813,7 @@ void testRejectWithoutDefaultMessage() { } @Test - void testBindExceptionSerializable() throws Exception { + void bindExceptionSerializable() throws Exception { SerializablePerson tb = new SerializablePerson(); tb.setName("myName"); tb.setAge(99); @@ -1849,27 +1842,27 @@ void testBindExceptionSerializable() throws Exception { } @Test - void testTrackDisallowedFields() { + void trackDisallowedFields() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAllowedFields("name", "age"); String name = "Rob Harrop"; - String beanName = "foobar"; + int age = 42; MutablePropertyValues mpvs = new MutablePropertyValues(); mpvs.add("name", name); - mpvs.add("beanName", beanName); + mpvs.add("age", age); + mpvs.add("beanName", "foobar"); binder.bind(mpvs); assertThat(testBean.getName()).isEqualTo(name); - String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); - assertThat(disallowedFields).hasSize(1); - assertThat(disallowedFields[0]).isEqualTo("beanName"); + assertThat(testBean.getAge()).isEqualTo(age); + assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("beanName"); } @Test - void testAutoGrowWithinDefaultLimit() { + void autoGrowWithinDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1881,7 +1874,7 @@ void testAutoGrowWithinDefaultLimit() { } @Test - void testAutoGrowBeyondDefaultLimit() { + void autoGrowBeyondDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1894,7 +1887,7 @@ void testAutoGrowBeyondDefaultLimit() { } @Test - void testAutoGrowWithinCustomLimit() { + void autoGrowWithinCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); @@ -1907,7 +1900,7 @@ void testAutoGrowWithinCustomLimit() { } @Test - void testAutoGrowBeyondCustomLimit() { + void autoGrowBeyondCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); @@ -1921,7 +1914,7 @@ void testAutoGrowBeyondCustomLimit() { } @Test - void testNestedGrowingList() { + void nestedGrowingList() { Form form = new Form(); DataBinder binder = new DataBinder(form, "form"); MutablePropertyValues mpv = new MutablePropertyValues(); @@ -1937,7 +1930,7 @@ void testNestedGrowingList() { } @Test - void testFieldErrorAccessVariations() { + void fieldErrorAccessVariations() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); assertThat(binder.getBindingResult().getGlobalError()).isNull(); @@ -1958,7 +1951,7 @@ void testFieldErrorAccessVariations() { } @Test // SPR-14888 - void testSetAutoGrowCollectionLimit() { + void setAutoGrowCollectionLimit() { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); binder.setAutoGrowCollectionLimit(257); @@ -1972,7 +1965,7 @@ void testSetAutoGrowCollectionLimit() { } @Test // SPR-14888 - void testSetAutoGrowCollectionLimitAfterInitialization() { + void setAutoGrowCollectionLimitAfterInitialization() { DataBinder binder = new DataBinder(new BeanWithIntegerList()); binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); assertThatIllegalStateException().isThrownBy(() -> @@ -1981,7 +1974,7 @@ void testSetAutoGrowCollectionLimitAfterInitialization() { } @Test // SPR-15009 - void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() { + void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); @@ -1998,7 +1991,7 @@ void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForBeanProper } @Test // SPR-15009 - void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() { + void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); @@ -2013,7 +2006,7 @@ void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFiel } @Test // SPR-15009 - void testSetCustomMessageCodesResolverAfterInitializeBindingResult() { + void setCustomMessageCodesResolverAfterInitializeBindingResult() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.initBeanPropertyAccess(); @@ -2028,7 +2021,7 @@ void testSetCustomMessageCodesResolverAfterInitializeBindingResult() { } @Test // SPR-15009 - void testSetMessageCodesResolverIsNullAfterInitializeBindingResult() { + void setMessageCodesResolverIsNullAfterInitializeBindingResult() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.initBeanPropertyAccess(); @@ -2042,8 +2035,7 @@ void testSetMessageCodesResolverIsNullAfterInitializeBindingResult() { } @Test // SPR-15009 - void testCallSetMessageCodesResolverTwice() { - + void callSetMessageCodesResolverTwice() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setMessageCodesResolver(new DefaultMessageCodesResolver()); diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 16bf30ee01fc..864e21843984 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,15 @@ * Special {@link org.springframework.validation.DataBinder} to perform data binding * from servlet request parameters to JavaBeans, including support for multipart files. * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * *

See the DataBinder/WebDataBinder superclasses for customization options, * which include specifying allowed/required fields, and registering custom * property editors. diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java index a1cd50ad7443..754f72ac5493 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,15 @@ * the Servlet API; serves as base class for more specific DataBinder variants, * such as {@link org.springframework.web.bind.ServletRequestDataBinder}. * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * *

Includes support for field markers which address a common problem with * HTML checkboxes and select options: detecting that a field was part of * the form, but did not generate a request parameter because it was empty. diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java index 3f48fa431185..467d9853ef2e 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java @@ -100,6 +100,7 @@ * @author Arjen Poutsma * @author Juergen Hoeller * @since 3.0 + * @see ControllerAdvice * @see org.springframework.web.context.request.WebRequest */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java index 5fc5d6bcc279..370b2f2801e0 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,15 +23,24 @@ import java.lang.annotation.Target; /** - * Annotation that identifies methods which initialize the + * Annotation that identifies methods that initialize the * {@link org.springframework.web.bind.WebDataBinder} which * will be used for populating command and form object arguments * of annotated handler methods. * - *

Such init-binder methods support all arguments that {@link RequestMapping} - * supports, except for command/form objects and corresponding validation result - * objects. Init-binder methods must not have a return value; they are usually - * declared as {@code void}. + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * + *

{@code @InitBinder} methods support all arguments that + * {@link RequestMapping @RequestMapping} methods support, except for command/form + * objects and corresponding validation result objects. {@code @InitBinder} methods + * must not have a return value; they are usually declared as {@code void}. * *

Typical arguments are {@link org.springframework.web.bind.WebDataBinder} * in combination with {@link org.springframework.web.context.request.WebRequest} @@ -39,6 +48,7 @@ * * @author Juergen Hoeller * @since 2.5 + * @see ControllerAdvice * @see org.springframework.web.bind.WebDataBinder * @see org.springframework.web.context.request.WebRequest */ diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java index 717a6d0106ef..3316065a0760 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author 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,18 +31,27 @@ * for controller classes with {@link RequestMapping @RequestMapping} * methods. * - *

Can be used to expose command objects to a web view, using - * specific attribute names, through annotating corresponding - * parameters of an {@link RequestMapping @RequestMapping} method. + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. * - *

Can also be used to expose reference data to a web view - * through annotating accessor methods in a controller class with + *

{@code @ModelAttribute} can be used to expose command objects to a web view, + * using specific attribute names, by annotating corresponding parameters of an + * {@link RequestMapping @RequestMapping} method. + * + *

{@code @ModelAttribute} can also be used to expose reference data to a web + * view by annotating accessor methods in a controller class with * {@link RequestMapping @RequestMapping} methods. Such accessor * methods are allowed to have any arguments that * {@link RequestMapping @RequestMapping} methods support, returning * the model attribute value to expose. * - *

Note however that reference data and all other model content is + *

Note however that reference data and all other model content are * not available to web views when request processing results in an * {@code Exception} since the exception could be raised at any time * making the content of the model unreliable. For this reason @@ -52,6 +61,7 @@ * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 2.5 + * @see ControllerAdvice */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @@ -77,7 +87,7 @@ String name() default ""; /** - * Allows declaring data binding disabled directly on an {@code @ModelAttribute} + * Allows data binding to be disabled directly on an {@code @ModelAttribute} * method parameter or on the attribute returned from an {@code @ModelAttribute} * method, both of which would prevent data binding for that attribute. *

By default this is set to {@code true} in which case data binding applies. 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 ed7855e79097..b4957970a5d2 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-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,15 @@ * Specialized {@link org.springframework.validation.DataBinder} to perform data * binding from URL query parameters or form data in the request data to Java objects. * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 5.0 diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index 76ea4abddab4..805667f69b87 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author 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,6 +35,15 @@ * Special {@link org.springframework.validation.DataBinder} to perform data binding * from web request parameters to JavaBeans, including support for multipart files. * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * *

See the DataBinder/WebDataBinder superclasses for customization options, * which include specifying allowed/required fields, and registering custom * property editors. diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java index 1126d2bf2516..ce284f935e71 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,7 +116,7 @@ public void createBinderTypeConversion() throws Exception { WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo"); assertThat(dataBinder.getDisallowedFields()).isNotNull(); - assertThat(dataBinder.getDisallowedFields()[0]).isEqualTo("requestParam-22"); + assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22"); } private WebDataBinderFactory createFactory(String methodName, Class... parameterTypes) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index 56ba84873cca..d695a3f750c6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,7 +121,7 @@ public void createBinderTypeConversion() throws Exception { WebDataBinder dataBinder = context.createDataBinder(exchange, null, "foo"); assertThat(dataBinder.getDisallowedFields()).isNotNull(); - assertThat(dataBinder.getDisallowedFields()[0]).isEqualTo("requestParam-22"); + assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22"); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index 2a7489b98176..662f1e722991 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,15 @@ * Subclass of {@link ServletRequestDataBinder} that adds URI template variables * to the values used for data binding. * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * * @author Rossen Stoyanchev * @since 3.1 * @see ServletRequestDataBinder diff --git a/src/docs/asciidoc/web/web-data-binding-model-design.adoc b/src/docs/asciidoc/web/web-data-binding-model-design.adoc new file mode 100644 index 000000000000..352e63d3c6f3 --- /dev/null +++ b/src/docs/asciidoc/web/web-data-binding-model-design.adoc @@ -0,0 +1,95 @@ +In the context of web applications, _data binding_ involves the binding of HTTP request +parameters (that is, form data or query parameters) to properties in a model object and +its nested objects. + +Only `public` properties following the +https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions] +are exposed for data binding — for example, `public String getFirstName()` and +`public void setFirstName(String)` methods for a `firstName` property. + +TIP: The model object, and its nested object graph, is also sometimes referred to as a +_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object). + +By default, Spring permits binding to all public properties in the model object graph. +This means you need to carefully consider what public properties the model has, since a +client could target any public property path, even some that are not expected to be +targeted for a given use case. + +For example, given an HTTP form data endpoint, a malicious client could supply values for +properties that exist in the model object graph but are not part of the HTML form +presented in the browser. This could lead to data being set on the model object and any +of its nested objects, that is not expected to be updated. + +The recommended approach is to use a _dedicated model object_ that exposes only +properties that are relevant for the form submission. For example, on a form for changing +a user's email address, the model object should declare a minimum set of properties such +as in the following `ChangeEmailForm`. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + public class ChangeEmailForm { + + private String oldEmailAddress; + private String newEmailAddress; + + public void setOldEmailAddress(String oldEmailAddress) { + this.oldEmailAddress = oldEmailAddress; + } + + public String getOldEmailAddress() { + return this.oldEmailAddress; + } + + public void setNewEmailAddress(String newEmailAddress) { + this.newEmailAddress = newEmailAddress; + } + + public String getNewEmailAddress() { + return this.newEmailAddress; + } + + } +---- + +If you cannot or do not want to use a _dedicated model object_ for each data +binding use case, you **must** limit the properties that are allowed for data binding. +Ideally, you can achieve this by registering _allowed field patterns_ via the +`setAllowedFields()` method on `WebDataBinder`. + +For example, to register allowed field patterns in your application, you can implement an +`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class ChangeEmailController { + + @InitBinder + void initBinder(WebDataBinder binder) { + binder.setAllowedFields("oldEmailAddress", "newEmailAddress"); + } + + // @RequestMapping methods, etc. + + } +---- + +In addition to registering allowed patterns, it is also possible to register _disallowed +field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses. +Please note, however, that an "allow list" is safer than a "deny list". Consequently, +`setAllowedFields()` should be favored over `setDisallowedFields()`. + +Note that matching against allowed field patterns is case-sensitive; whereas, matching +against disallowed field patterns is case-insensitive. In addition, a field matching a +disallowed pattern will not be accepted even if it also happens to match a pattern in the +allowed list. + +[WARNING] +==== +It is extremely important to properly configure allowed and disallowed field patterns +when exposing your domain model directly for data binding purposes. Otherwise, it is a +big security risk. + +Furthermore, it is strongly recommended that you do **not** use types from your domain +model such as JPA or Hibernate entities as the model object in data binding scenarios. +==== diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 111980c64616..2276f15aadf9 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -3319,6 +3319,11 @@ controller-specific `Formatter` instances, as the following example shows: ---- <1> Adding a custom formatter (a `DateFormatter`, in this case). +[[webflux-ann-initbinder-model-design]] +==== Model Design +[.small]#<># + +include::web-data-binding-model-design.adoc[] [[webflux-ann-controller-exceptions]] diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 705c33a5d2a7..30a3867b00b6 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -3751,6 +3751,13 @@ controller-specific `Formatter` implementations, as the following example shows: ---- <1> Defining an `@InitBinder` method on a custom formatter. +[[mvc-ann-initbinder-model-design]] +==== Model Design +[.small]#<># + +include::web-data-binding-model-design.adoc[] + + [[mvc-ann-exceptionhandler]] === Exceptions [.small]#<># From 9e733b09e2c167c873da66a334a8ca0e23c8f33b Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Wed, 13 Apr 2022 09:24:33 +0000 Subject: [PATCH 25/64] Next development version (v5.3.20-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 03750b799538..a03487da791a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.19-SNAPSHOT +version=5.3.20-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From f54952481b8d6388d6edb222254a0f8dfc43e578 Mon Sep 17 00:00:00 2001 From: Jerome Prinet Date: Thu, 14 Apr 2022 09:27:55 +0200 Subject: [PATCH 26/64] Update Gradle Enterprise plugin to 3.9 Closes gh-28338 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index e1638d26c9ad..624fa2d4c340 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.8.1" + id "com.gradle.enterprise" version "3.9" id "io.spring.ge.conventions" version "0.0.9" } From 22c82ff206b417074e9657cac3b557fe3a2dbb24 Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Thu, 14 Apr 2022 13:31:02 +0200 Subject: [PATCH 27/64] Fix method reference in Kotlin documentation See gh-28340 --- .../beans/factory/ListableBeanFactoryExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt index bc0edd4948b6..2ad85e33311c 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt @@ -39,7 +39,7 @@ inline fun ListableBeanFactory.getBeansOfType(includeNonSingle /** * Extension for [ListableBeanFactory.getBeanNamesForAnnotation] providing a - * `getBeansOfType()` variant. + * `getBeanNamesForAnnotation()` variant. * * @author Sebastien Deleuze * @since 5.0 From 3017955eff827facc3e680422ba4f0103a545cb6 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Apr 2022 14:09:54 +0200 Subject: [PATCH 28/64] Update copyright year of changed file See gh-28340 --- .../beans/factory/ListableBeanFactoryExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt index 2ad85e33311c..00ed65c25277 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From ca1a11acacd85a3f049b32299991cb08c30557ce Mon Sep 17 00:00:00 2001 From: zhangmingqi09 Date: Sun, 24 Apr 2022 14:00:51 +0800 Subject: [PATCH 29/64] Fix github issue reference in RequestMappingHandlerMapping See gh-28372 --- .../mvc/method/annotation/RequestMappingHandlerMapping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index c3b264158891..f297231e6d35 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -66,7 +66,7 @@ * mapping and for content negotiation (with similar deprecations in * {@link org.springframework.web.accept.ContentNegotiationManagerFactoryBean * ContentNegotiationManagerFactoryBean}). For further context, please read issue - * #24719. + * #24179. * * @author Arjen Poutsma * @author Rossen Stoyanchev From b81c62d064f7b78fcca5deb973b10f6e49dc9af4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Sun, 24 Apr 2022 09:56:39 +0200 Subject: [PATCH 30/64] Update copyright year of changed file See gh-28372 --- .../mvc/method/annotation/RequestMappingHandlerMapping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index f297231e6d35..d550f9a17941 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From fcf64798b518149736983aa2ad1308d474a04664 Mon Sep 17 00:00:00 2001 From: izeye Date: Sat, 23 Apr 2022 14:58:01 +0900 Subject: [PATCH 31/64] Add Javadoc since for GraphQL constants See gh-28369 --- .../main/java/org/springframework/util/MimeTypeUtils.java | 6 ++++-- .../src/main/java/org/springframework/http/MediaType.java | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index c7c5468b5896..ed4919e87bc0 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -67,18 +67,20 @@ public abstract class MimeTypeUtils { /** * Public constant mime type for {@code application/graphql+json}. + * @since 5.3.19 * @see GraphQL over HTTP spec - * */ + */ public static final MimeType APPLICATION_GRAPHQL; /** * A String equivalent of {@link MimeTypeUtils#APPLICATION_GRAPHQL}. + * @since 5.3.19 */ public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; /** * Public constant mime type for {@code application/json}. - * */ + */ public static final MimeType APPLICATION_JSON; /** diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 2a784e0386b1..c30efd4c3f0e 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -97,12 +97,14 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/graphql+json}. + * @since 5.3.19 * @see GraphQL over HTTP spec */ public static final MediaType APPLICATION_GRAPHQL; /** * A String equivalent of {@link MediaType#APPLICATION_GRAPHQL}. + * @since 5.3.19 */ public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; From b6b03f38d7d767ded06b2f9387df3c868b675c54 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Apr 2022 11:54:02 +0200 Subject: [PATCH 32/64] Updated .sdkmanrc --- .sdkmanrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index a59545673245..51f59b27450b 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=8.0.322-librca +java=8.0.332-librca From b30f4d7bb770be1d13171ffd4a394635f8dbecdd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 25 Apr 2022 16:01:02 +0100 Subject: [PATCH 33/64] Exposes all root causes to ExceptionHandler methods Closes gh-28155 --- .../RequestMappingHandlerAdapter.java | 26 +++++++++++++------ .../annotation/ControllerAdviceTests.java | 9 ++++--- ...pingExceptionHandlingIntegrationTests.java | 4 +-- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 094c8a728752..7f970f670b6d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.web.reactive.result.method.annotation; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -213,21 +214,30 @@ private Mono handleException(Throwable exception, HandlerMethod h InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod); if (invocable != null) { + ArrayList exceptions = new ArrayList<>(); try { if (logger.isDebugEnabled()) { logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable); } bindingContext.getModel().asMap().clear(); - Throwable cause = exception.getCause(); - if (cause != null) { - return invocable.invoke(exchange, bindingContext, exception, cause, handlerMethod); - } - else { - return invocable.invoke(exchange, bindingContext, exception, handlerMethod); + + // Expose causes as provided arguments as well + Throwable exToExpose = exception; + while (exToExpose != null) { + exceptions.add(exToExpose); + Throwable cause = exToExpose.getCause(); + exToExpose = (cause != exToExpose ? cause : null); } + Object[] arguments = new Object[exceptions.size() + 1]; + exceptions.toArray(arguments); // efficient arraycopy call in ArrayList + arguments[arguments.length - 1] = handlerMethod; + + return invocable.invoke(exchange, bindingContext, arguments); } catch (Throwable invocationEx) { - if (logger.isWarnEnabled()) { + // Any other than the original exception (or a cause) is unintended here, + // probably an accident (e.g. failed assertion or the like). + if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java index ec0e73ee02cb..d506d2af4b69 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerAdviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,9 +82,10 @@ public void resolveExceptionWithAssertionError() throws Exception { @Test public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception { - AssertionError cause = new AssertionError("argh"); - FatalBeanException exception = new FatalBeanException("wrapped", cause); - testException(exception, cause.toString()); + AssertionError rootCause = new AssertionError("argh"); + FatalBeanException cause = new FatalBeanException("wrapped", rootCause); + Exception exception = new Exception(cause); + testException(exception, rootCause.toString()); } private void testException(Throwable exception, String expected) throws Exception { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java index 7acff0afd13b..2dbf712d7962 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author 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,7 @@ public Publisher handleAndThrowExceptionWithCause() { @GetMapping("/thrown-exception-with-cause-to-handle") public Publisher handleAndThrowExceptionWithCauseToHandle() { - throw new RuntimeException("State", new IOException("IO")); + throw new RuntimeException("State1", new RuntimeException("State2", new IOException("IO"))); } @GetMapping(path = "/mono-error") From caaf83b8e6534b8c9a3806661022ac8185985876 Mon Sep 17 00:00:00 2001 From: binchoo <079111w@gmail.com> Date: Wed, 27 Apr 2022 12:49:05 +0900 Subject: [PATCH 34/64] Add tests for binding to a Part field See gh-27830 --- .../standalone/MultipartControllerTests.java | 109 +++++++++++++++++- .../web/bind/ServletRequestDataBinder.java | 5 +- .../bind/support/WebRequestDataBinder.java | 3 +- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index a00dd2774e3a..141f8072c156 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -39,6 +39,9 @@ import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; import org.springframework.ui.Model; +import org.springframework.util.StreamUtils; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -56,6 +59,7 @@ /** * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Jaebin Joo */ public class MultipartControllerTests { @@ -225,7 +229,7 @@ public void multipartRequestWithOptionalFileListNotPresent() throws Exception { } @Test - public void multipartRequestWithServletParts() throws Exception { + public void multipartRequestWithParts_resolvesMultipartFileArguments() throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); MockPart filePart = new MockPart("file", "orig", fileContent); @@ -240,6 +244,50 @@ public void multipartRequestWithServletParts() throws Exception { .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); } + @Test + public void multipartRequestWithParts_resolvesPartArguments() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); + MockPart jsonPart = new MockPart("json", json); + jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/part").part(filePart).part(jsonPart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); + } + + @Test + public void multipartRequestWithParts_resolvesMultipartFileProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + standaloneSetup(new MultipartController()).build() + .perform(multipart("/multipartfileproperty").part(filePart)) + .andExpect(status().isFound()) + .andExpect(model().attribute("fileContent", fileContent)); + } + + @Test + public void multipartRequestWithParts_cannotResolvePartProperties() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + MockPart filePart = new MockPart("file", "orig", fileContent); + + Exception exception = standaloneSetup(new MultipartController()).build() + .perform(multipart("/partproperty").part(filePart)) + .andExpect(status().is4xxClientError()) + .andReturn() + .getResolvedException(); + + assertThat(exception).isNotNull(); + assertThat(exception).isInstanceOf(BindException.class); + assertThat(((BindException) exception).getFieldError("file")) + .as("MultipartRequest would not bind Part properties.").isNotNull(); + } + @Test // SPR-13317 public void multipartRequestWrapped() throws Exception { byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); @@ -343,10 +391,13 @@ public String processOptionalFileList(@RequestParam Optional } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestParam Part part, + public String processPart(@RequestPart Part file, @RequestPart Map json, Model model) throws IOException { - model.addAttribute("fileContent", part.getInputStream()); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } model.addAttribute("jsonContent", json); return "redirect:/index"; @@ -357,8 +408,60 @@ public String processMultipart(@RequestPart Map json, Model mode model.addAttribute("json", json); return "redirect:/index"; } + + @RequestMapping(value = "/multipartfileproperty", method = RequestMethod.POST) + public String processMultipartFileBean(MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + MultipartFile file = multipartFileBean.getFile(); + if (file != null) { + model.addAttribute("fileContent", file.getBytes()); + } + } + return "redirect:/index"; + } + + @RequestMapping(value = "/partproperty", method = RequestMethod.POST) + public String processPartBean(PartBean partBean, Model model, BindingResult bindingResult) + throws IOException { + + if (!bindingResult.hasErrors()) { + Part file = partBean.getFile(); + if (file != null) { + byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + model.addAttribute("fileContent", content); + } + } + return "redirect:/index"; + } } + private static class MultipartFileBean { + + private MultipartFile file; + + public MultipartFile getFile() { + return file; + } + + public void setFile(MultipartFile file) { + this.file = file; + } + } + + private static class PartBean { + + private Part file; + + public Part getFile() { + return file; + } + + public void setFile(Part file) { + this.file = file; + } + } private static class RequestWrappingFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 864e21843984..24c37ce3cbcf 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -104,11 +104,14 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * Part, byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartHttpServletRequest + * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile + * @see jakarta.servlet.http.Part * @see #bind(org.springframework.beans.PropertyValues) */ public void bind(ServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index 805667f69b87..ed0d1b305127 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -108,7 +108,8 @@ public WebRequestDataBinder(@Nullable Object target, String objectName) { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be Part, MultipartFile, - * byte[], or String. The latter two receive the contents of the uploaded file; + * byte[], or String. The Part binding is only supported when the request + * is not a MultipartRequest. The latter two receive the contents of the uploaded file; * all metadata like original file name, content type, etc are lost in those cases. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest From f0d149b33066759e21a4c39ee3ce90cb23320fc3 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 28 Apr 2022 11:26:50 +0100 Subject: [PATCH 35/64] Polishing contribution Closes gh-27830 --- .../standalone/MultipartControllerTests.java | 110 ++++-------------- .../web/bind/ServletRequestDataBinder.java | 5 +- .../bind/support/WebRequestDataBinder.java | 7 +- 3 files changed, 25 insertions(+), 97 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index 141f8072c156..67400f7dac98 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author 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,15 +32,16 @@ import javax.servlet.http.Part; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockPart; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.ui.Model; import org.springframework.util.StreamUtils; -import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -63,16 +64,20 @@ */ public class MultipartControllerTests { - @Test - public void multipartRequestWithSingleFile() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"/multipartfile", "/part"}) + public void multipartRequestWithSingleFileOrPart(String url) throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockMultipartFile filePart = new MockMultipartFile("file", "orig", null, fileContent); byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); MockMultipartFile jsonPart = new MockMultipartFile("json", "json", "application/json", json); + MockMultipartHttpServletRequestBuilder requestBuilder = (url.endsWith("file") ? + multipart(url).file(new MockMultipartFile("file", "orig", null, fileContent)) : + multipart(url).part(new MockPart("part", "orig", fileContent))); + standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfile").file(filePart).file(jsonPart)) + .perform(requestBuilder.file(jsonPart)) .andExpect(status().isFound()) .andExpect(model().attribute("fileContent", fileContent)) .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); @@ -229,65 +234,16 @@ public void multipartRequestWithOptionalFileListNotPresent() throws Exception { } @Test - public void multipartRequestWithParts_resolvesMultipartFileArguments() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); - MockPart jsonPart = new MockPart("json", json); - jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); - - standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfile").part(filePart).part(jsonPart)) - .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); - } - - @Test - public void multipartRequestWithParts_resolvesPartArguments() throws Exception { + public void multipartRequestWithDataBindingToFile() throws Exception { byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); MockPart filePart = new MockPart("file", "orig", fileContent); - byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); - MockPart jsonPart = new MockPart("json", json); - jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); - standaloneSetup(new MultipartController()).build() - .perform(multipart("/part").part(filePart).part(jsonPart)) - .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); - } - - @Test - public void multipartRequestWithParts_resolvesMultipartFileProperties() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - standaloneSetup(new MultipartController()).build() - .perform(multipart("/multipartfileproperty").part(filePart)) + .perform(multipart("/multipartfilebinding").part(filePart)) .andExpect(status().isFound()) .andExpect(model().attribute("fileContent", fileContent)); } - @Test - public void multipartRequestWithParts_cannotResolvePartProperties() throws Exception { - byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); - MockPart filePart = new MockPart("file", "orig", fileContent); - - Exception exception = standaloneSetup(new MultipartController()).build() - .perform(multipart("/partproperty").part(filePart)) - .andExpect(status().is4xxClientError()) - .andReturn() - .getResolvedException(); - - assertThat(exception).isNotNull(); - assertThat(exception).isInstanceOf(BindException.class); - assertThat(((BindException) exception).getFieldError("file")) - .as("MultipartRequest would not bind Part properties.").isNotNull(); - } - @Test // SPR-13317 public void multipartRequestWrapped() throws Exception { byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8); @@ -391,11 +347,11 @@ public String processOptionalFileList(@RequestParam Optional } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestPart Part file, + public String processPart(@RequestPart Part part, @RequestPart Map json, Model model) throws IOException { - if (file != null) { - byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); + if (part != null) { + byte[] content = StreamUtils.copyToByteArray(part.getInputStream()); model.addAttribute("fileContent", content); } model.addAttribute("jsonContent", json); @@ -409,9 +365,9 @@ public String processMultipart(@RequestPart Map json, Model mode return "redirect:/index"; } - @RequestMapping(value = "/multipartfileproperty", method = RequestMethod.POST) - public String processMultipartFileBean(MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) - throws IOException { + @RequestMapping(value = "/multipartfilebinding", method = RequestMethod.POST) + public String processMultipartFileBean( + MultipartFileBean multipartFileBean, Model model, BindingResult bindingResult) throws IOException { if (!bindingResult.hasErrors()) { MultipartFile file = multipartFileBean.getFile(); @@ -421,20 +377,6 @@ public String processMultipartFileBean(MultipartFileBean multipartFileBean, Mode } return "redirect:/index"; } - - @RequestMapping(value = "/partproperty", method = RequestMethod.POST) - public String processPartBean(PartBean partBean, Model model, BindingResult bindingResult) - throws IOException { - - if (!bindingResult.hasErrors()) { - Part file = partBean.getFile(); - if (file != null) { - byte[] content = StreamUtils.copyToByteArray(file.getInputStream()); - model.addAttribute("fileContent", content); - } - } - return "redirect:/index"; - } } private static class MultipartFileBean { @@ -450,18 +392,6 @@ public void setFile(MultipartFile file) { } } - private static class PartBean { - - private Part file; - - public Part getFile() { - return file; - } - - public void setFile(Part file) { - this.file = file; - } - } private static class RequestWrappingFilter extends OncePerRequestFilter { diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 24c37ce3cbcf..1c6f0218d2e7 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -104,9 +104,8 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) { * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. *

The type of the target property for a multipart file can be MultipartFile, - * Part, byte[], or String. The Part binding is only supported when the request - * is not a MultipartRequest. The latter two receive the contents of the uploaded file; - * all metadata like original file name, content type, etc are lost in those cases. + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartHttpServletRequest * @see org.springframework.web.multipart.MultipartRequest diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index ed0d1b305127..16f6141cbd24 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -107,10 +107,9 @@ public WebRequestDataBinder(@Nullable Object target, String objectName) { *

Multipart files are bound via their parameter name, just like normal * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, * invoking a "setUploadedFile" setter method. - *

The type of the target property for a multipart file can be Part, MultipartFile, - * byte[], or String. The Part binding is only supported when the request - * is not a MultipartRequest. The latter two receive the contents of the uploaded file; - * all metadata like original file name, content type, etc are lost in those cases. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. Servlet Part binding is also supported when the + * request has not been parsed to MultipartRequest via MultipartResolver. * @param request the request with parameters to bind (can be multipart) * @see org.springframework.web.multipart.MultipartRequest * @see org.springframework.web.multipart.MultipartFile From afa799b4f0c48275089cdba2aac16b9ee589e422 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 3 May 2022 11:46:47 +0200 Subject: [PATCH 36/64] Suppress warning in test --- .../web/servlet/samples/standalone/MultipartControllerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java index 67400f7dac98..f525ac797125 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/MultipartControllerTests.java @@ -387,6 +387,7 @@ public MultipartFile getFile() { return file; } + @SuppressWarnings("unused") public void setFile(MultipartFile file) { this.file = file; } From ed06a6de26f60adf36f448450e9325958e516951 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 3 May 2022 12:03:04 +0200 Subject: [PATCH 37/64] Convert SimpleFormController example to @Controller in reference manual This change is necessary since the SimpleFormController class no longer exists. --- src/docs/asciidoc/core/core-validation.adoc | 39 +++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/docs/asciidoc/core/core-validation.adoc b/src/docs/asciidoc/core/core-validation.adoc index cded5e664ee0..63a708256fbb 100644 --- a/src/docs/asciidoc/core/core-validation.adoc +++ b/src/docs/asciidoc/core/core-validation.adoc @@ -792,8 +792,8 @@ See also the `org.springframework.beans.support.ResourceEditorRegistrar` for an `PropertyEditorRegistrar` implementation. Notice how in its implementation of the `registerCustomEditors(..)` method, it creates new instances of each property editor. -The next example shows how to configure a `CustomEditorConfigurer` and inject an instance of our -`CustomPropertyEditorRegistrar` into it: +The next example shows how to configure a `CustomEditorConfigurer` and inject an instance +of our `CustomPropertyEditorRegistrar` into it: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -809,50 +809,51 @@ The next example shows how to configure a `CustomEditorConfigurer` and inject an class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/> ---- -Finally (and in a bit of a departure from the focus of this chapter for those of you -using <>), using `PropertyEditorRegistrars` in -conjunction with data-binding `Controllers` (such as `SimpleFormController`) can be very -convenient. The following example uses a `PropertyEditorRegistrar` in the -implementation of an `initBinder(..)` method: +Finally (and in a bit of a departure from the focus of this chapter) for those of you +using <>, using a `PropertyEditorRegistrar` in +conjunction with data-binding web controllers can be very convenient. The following +example uses a `PropertyEditorRegistrar` in the implementation of an `@InitBinder` method: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java ---- - public final class RegisterUserController extends SimpleFormController { + @Controller + public class RegisterUserController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; - public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { + RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { this.customPropertyEditorRegistrar = propertyEditorRegistrar; } - protected void initBinder(HttpServletRequest request, - ServletRequestDataBinder binder) throws Exception { + @InitBinder + void initBinder(WebDataBinder binder) { this.customPropertyEditorRegistrar.registerCustomEditors(binder); } - // other methods to do with registering a User + // other methods related to registering a User } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- + @Controller class RegisterUserController( - private val customPropertyEditorRegistrar: PropertyEditorRegistrar) : SimpleFormController() { + private val customPropertyEditorRegistrar: PropertyEditorRegistrar) { - protected fun initBinder(request: HttpServletRequest, - binder: ServletRequestDataBinder) { + @InitBinder + fun initBinder(binder: WebDataBinder) { this.customPropertyEditorRegistrar.registerCustomEditors(binder) } - // other methods to do with registering a User + // other methods related to registering a User } ---- This style of `PropertyEditor` registration can lead to concise code (the implementation -of `initBinder(..)` is only one line long) and lets common `PropertyEditor` -registration code be encapsulated in a class and then shared amongst as many -`Controllers` as needed. +of the `@InitBinder` method is only one line long) and lets common `PropertyEditor` +registration code be encapsulated in a class and then shared amongst as many controllers +as needed. From 4fcfa5b9916ddc06a84ed4d6705bd1e68b3eb6b5 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 3 May 2022 14:27:48 +0200 Subject: [PATCH 38/64] Update and simplify ArrayConstructorTests --- .../spel/ArrayConstructorTests.java | 170 +++++------------- 1 file changed, 45 insertions(+), 125 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java index 267b45d26c9f..252e532e748f 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -27,45 +28,33 @@ * Test construction of arrays. * * @author Andy Clement + * @author Sam Brannen */ -public class ArrayConstructorTests extends AbstractExpressionTests { +class ArrayConstructorTests extends AbstractExpressionTests { @Test - public void simpleArrayWithInitializer() { - evaluateArrayBuildingExpression("new int[]{1,2,3}", "[1,2,3]"); - evaluateArrayBuildingExpression("new int[]{}", "[]"); - evaluate("new int[]{}.length", "0", Integer.class); - } - - @Test - public void conversion() { + void conversion() { evaluate("new String[]{1,2,3}[0]", "1", String.class); evaluate("new int[]{'123'}[0]", 123, Integer.class); } @Test - public void multidimensionalArrays() { - evaluateAndCheckError("new int[][]{{1,2},{3,4}}", SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); - evaluateAndCheckError("new int[3][]", SpelMessage.MISSING_ARRAY_DIMENSION); - evaluateAndCheckError("new int[]", SpelMessage.MISSING_ARRAY_DIMENSION); - evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); - evaluateAndCheckError("new int[][1]", SpelMessage.MISSING_ARRAY_DIMENSION); - } + void primitiveTypeArrayConstructors() { + evaluateArrayBuildingExpression("new int[]{}", "{}"); + evaluateArrayBuildingExpression("new int[]{1,2,3,4}", "{1, 2, 3, 4}"); + evaluateArrayBuildingExpression("new boolean[]{true,false,true}", "{true, false, true}"); + evaluateArrayBuildingExpression("new char[]{'a','b','c'}", "{'a', 'b', 'c'}"); + evaluateArrayBuildingExpression("new long[]{1,2,3,4,5}", "{1, 2, 3, 4, 5}"); + evaluateArrayBuildingExpression("new short[]{2,3,4,5,6}", "{2, 3, 4, 5, 6}"); + evaluateArrayBuildingExpression("new double[]{1d,2d,3d,4d}", "{1.0, 2.0, 3.0, 4.0}"); + evaluateArrayBuildingExpression("new float[]{1f,2f,3f,4f}", "{1.0, 2.0, 3.0, 4.0}"); + evaluateArrayBuildingExpression("new byte[]{1,2,3,4}", "{1, 2, 3, 4}"); - @Test - public void primitiveTypeArrayConstructors() { - evaluateArrayBuildingExpression("new int[]{1,2,3,4}", "[1,2,3,4]"); - evaluateArrayBuildingExpression("new boolean[]{true,false,true}", "[true,false,true]"); - evaluateArrayBuildingExpression("new char[]{'a','b','c'}", "[a,b,c]"); - evaluateArrayBuildingExpression("new long[]{1,2,3,4,5}", "[1,2,3,4,5]"); - evaluateArrayBuildingExpression("new short[]{2,3,4,5,6}", "[2,3,4,5,6]"); - evaluateArrayBuildingExpression("new double[]{1d,2d,3d,4d}", "[1.0,2.0,3.0,4.0]"); - evaluateArrayBuildingExpression("new float[]{1f,2f,3f,4f}", "[1.0,2.0,3.0,4.0]"); - evaluateArrayBuildingExpression("new byte[]{1,2,3,4}", "[1,2,3,4]"); + evaluate("new int[]{}.length", "0", Integer.class); } @Test - public void primitiveTypeArrayConstructorsElements() { + void primitiveTypeArrayConstructorsElements() { evaluate("new int[]{1,2,3,4}[0]", 1, Integer.class); evaluate("new boolean[]{true,false,true}[0]", true, Boolean.class); evaluate("new char[]{'a','b','c'}[0]", 'a', Character.class); @@ -78,15 +67,34 @@ public void primitiveTypeArrayConstructorsElements() { } @Test - public void errorCases() { + void errorCases() { + evaluateAndCheckError("new int[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[3][]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[][1]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new char[7]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); evaluateAndCheckError("new char[3]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); + + evaluateAndCheckError("new int[][]{{1,2},{3,4}}", SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); + evaluateAndCheckError("new char[2]{'hello','world'}", SpelMessage.TYPE_CONVERSION_ERROR); + // Could conceivably be a SpelMessage.INCORRECT_ELEMENT_TYPE_FOR_ARRAY, but it appears + // that SpelMessage.INCORRECT_ELEMENT_TYPE_FOR_ARRAY is not actually (no longer?) used + // in the code base. + evaluateAndCheckError("new Integer[3]{'3','ghi','5'}", SpelMessage.TYPE_CONVERSION_ERROR); + evaluateAndCheckError("new String('a','c','d')", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); + // Root cause: java.lang.OutOfMemoryError: Requested array size exceeds VM limit + evaluateAndCheckError("new java.util.ArrayList(T(java.lang.Integer).MAX_VALUE)", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); + + int threshold = 256 * 1024; // ConstructorReference.MAX_ARRAY_ELEMENTS + evaluateAndCheckError("new int[T(java.lang.Integer).MAX_VALUE]", SpelMessage.MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED, 0, threshold); + evaluateAndCheckError("new int[1024 * 1024][1024 * 1024]", SpelMessage.MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED, 0, threshold); } @Test - public void typeArrayConstructors() { + void typeArrayConstructors() { evaluate("new String[]{'a','b','c','d'}[1]", "b", String.class); evaluateAndCheckError("new String[]{'a','b','c','d'}.size()", SpelMessage.METHOD_NOT_FOUND, 30, "size()", "java.lang.String[]"); @@ -94,113 +102,25 @@ public void typeArrayConstructors() { } @Test - public void basicArray() { + void basicArray() { evaluate("new String[3]", "java.lang.String[3]{null,null,null}", String[].class); } @Test - public void multiDimensionalArray() { + void multiDimensionalArrays() { evaluate("new String[2][2]", "[Ljava.lang.String;[2]{[2]{null,null},[2]{null,null}}", String[][].class); evaluate("new String[3][2][1]", "[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}", String[][][].class); } - @Test - public void constructorInvocation03() { - evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); - } - - public void constructorInvocation04() { - evaluateAndCheckError("new Integer[3]{'3','ghi','5'}", SpelMessage.INCORRECT_ELEMENT_TYPE_FOR_ARRAY, 4); - } - - private String evaluateArrayBuildingExpression(String expression, String expectedToString) { + private void evaluateArrayBuildingExpression(String expression, String expectedToString) { SpelExpressionParser parser = new SpelExpressionParser(); Expression e = parser.parseExpression(expression); - Object o = e.getValue(); - assertThat(o).isNotNull(); - assertThat(o.getClass().isArray()).isTrue(); - StringBuilder s = new StringBuilder(); - s.append('['); - if (o instanceof int[]) { - int[] array = (int[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof boolean[]) { - boolean[] array = (boolean[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof char[]) { - char[] array = (char[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof long[]) { - long[] array = (long[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof short[]) { - short[] array = (short[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof double[]) { - double[] array = (double[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof float[]) { - float[] array = (float[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else if (o instanceof byte[]) { - byte[] array = (byte[]) o; - for (int i = 0; i < array.length; i++) { - if (i > 0) { - s.append(','); - } - s.append(array[i]); - } - } - else { - throw new IllegalStateException("Not supported " + o.getClass()); - } - s.append(']'); - assertThat(s.toString()).isEqualTo(expectedToString); - return s.toString(); + Object array = e.getValue(); + assertThat(array).isNotNull(); + assertThat(array.getClass().isArray()).isTrue(); + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo(expectedToString); } } From dbdd67ec62c0314c90512f23278caca4bdd0b0a5 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 3 May 2022 15:05:58 +0200 Subject: [PATCH 39/64] Remove outdated docker-image resource from CI The docker-image resource is now replaced by the registry-image resource. --- ci/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 712bd2cac0de..ab806e3eaa98 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -92,7 +92,7 @@ resources: branch: ((branch)) paths: ["ci/images/*"] - name: ci-image - type: docker-image + type: registry-image icon: docker source: <<: *docker-resource-source From c056b8175344dc06ce1cbebf07d7f9d122053e7c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 3 May 2022 15:08:14 +0200 Subject: [PATCH 40/64] Upgrade JDK and Ubuntu versions in CI image --- ci/images/ci-image/Dockerfile | 2 +- ci/images/get-jdk-url.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index 37ab9b6edb47..2e2c9b9a7d60 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal-20220404 +FROM ubuntu:focal-20220415 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 3b0006dd7f6d..d8dc4e57c089 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,16 +3,16 @@ set -e case "$1" in java8) - echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jdk_x64_linux_hotspot_8u322b06.tar.gz" + echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u332-b09/OpenJDK8U-jdk_x64_linux_hotspot_8u332b09.tar.gz" ;; java11) - echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.14.1_1.tar.gz" + echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.15_10.tar.gz" ;; java17) - echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz" + echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.3%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.3_7.tar.gz" ;; java18) - echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz" + echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz" ;; *) echo $"Unknown java version" From 5b1719cd7762dd6756028433ac75326448ed01e0 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 3 May 2022 15:09:23 +0200 Subject: [PATCH 41/64] Upgrade CI to concourse-release-scripts 0.3.4 --- ci/images/setup.sh | 2 -- ci/parameters.yml | 1 + ci/pipeline.yml | 5 +---- ci/scripts/promote-version.sh | 4 ++-- ci/tasks/promote-version.yml | 7 ++++++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ci/images/setup.sh b/ci/images/setup.sh index 6c02f65ef665..f7add2e15cb0 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -14,8 +14,6 @@ rm -rf /var/lib/apt/lists/* curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh -curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.2/concourse-release-scripts-0.3.2.jar - ########################################################### # JAVA ########################################################### diff --git a/ci/parameters.yml b/ci/parameters.yml index 7f26578d27a2..2c45862cf41e 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -1,5 +1,6 @@ github-repo: "https://github.com/spring-projects/spring-framework.git" github-repo-name: "spring-projects/spring-framework" +sonatype-staging-profile: "org.springframework" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io/" branch: "5.3.x" diff --git a/ci/pipeline.yml b/ci/pipeline.yml index ab806e3eaa98..49ee3d0b4d92 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -12,7 +12,7 @@ anchors: SONATYPE_USERNAME: ((sonatype-username)) SONATYPE_PASSWORD: ((sonatype-password)) SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id)) + SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) artifactory-task-params: &artifactory-task-params ARTIFACTORY_SERVER: ((artifactory-server)) ARTIFACTORY_USERNAME: ((artifactory-username)) @@ -345,7 +345,6 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: M @@ -390,7 +389,6 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: RC @@ -435,7 +433,6 @@ jobs: download_artifacts: true save_build_info: true - task: promote - image: ci-image file: git-repo/ci/tasks/promote-version.yml params: RELEASE_TYPE: RELEASE diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh index 44c5ff626f91..2b932f5f20f9 100755 --- a/ci/scripts/promote-version.sh +++ b/ci/scripts/promote-version.sh @@ -6,11 +6,11 @@ CONFIG_DIR=git-repo/ci/config version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -java -jar /opt/concourse-release-scripts.jar \ +java -jar /concourse-release-scripts.jar \ --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } -java -jar /opt/concourse-release-scripts.jar \ +java -jar /concourse-release-scripts.jar \ --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } diff --git a/ci/tasks/promote-version.yml b/ci/tasks/promote-version.yml index abdd8fed5c5c..bd9b28610df0 100644 --- a/ci/tasks/promote-version.yml +++ b/ci/tasks/promote-version.yml @@ -1,5 +1,10 @@ --- platform: linux +image_resource: + type: registry-image + source: + repository: springio/concourse-release-scripts + tag: '0.3.4' inputs: - name: git-repo - name: artifactory-repo @@ -13,6 +18,6 @@ params: SONATYPE_USER: SONATYPE_PASSWORD: SONATYPE_URL: - SONATYPE_STAGING_PROFILE_ID: + SONATYPE_STAGING_PROFILE: run: path: git-repo/ci/scripts/promote-version.sh From 7aedb9ee33764056f874e6f8a191af89ce5f8903 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 3 May 2022 15:50:29 +0200 Subject: [PATCH 42/64] Build CI image using oci-build-task resource --- ci/parameters.yml | 3 +++ ci/pipeline.yml | 21 ++++++++++++++----- ci/scripts/sync-to-maven-central.sh | 8 -------- ci/tasks/build-ci-image.yml | 31 +++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 13 deletions(-) delete mode 100755 ci/scripts/sync-to-maven-central.sh create mode 100644 ci/tasks/build-ci-image.yml diff --git a/ci/parameters.yml b/ci/parameters.yml index 2c45862cf41e..2f970570a230 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -8,4 +8,7 @@ milestone: "5.3.x" build-name: "spring-framework" pipeline-name: "spring-framework" concourse-url: "https://ci.spring.io/" +registry-mirror-host: docker.repo.spring.io +registry-mirror-username: ((artifactory-username)) +registry-mirror-password: ((artifactory-password)) task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 49ee3d0b4d92..5f7e91ba4661 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -24,6 +24,10 @@ anchors: username: ((docker-hub-username)) password: ((docker-hub-password)) tag: ((milestone)) + registry-mirror-vars: ®istry-mirror-vars + registry-mirror-host: ((registry-mirror-host)) + registry-mirror-username: ((registry-mirror-username)) + registry-mirror-password: ((registry-mirror-password)) slack-fail-params: &slack-fail-params text: > :concourse-failed: @@ -162,13 +166,20 @@ resources: jobs: - name: build-ci-images plan: - - get: ci-images-git-repo - trigger: true - - in_parallel: + - get: git-repo + - get: ci-images-git-repo + trigger: true + - task: build-ci-image + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image + vars: + ci-image-name: ci-image + <<: *registry-mirror-vars - put: ci-image params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/ci-image/Dockerfile + image: ci-image/image.tar - name: build serial: true public: true diff --git a/ci/scripts/sync-to-maven-central.sh b/ci/scripts/sync-to-maven-central.sh deleted file mode 100755 index b42631164ed5..000000000000 --- a/ci/scripts/sync-to-maven-central.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) -java -jar /opt/concourse-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; } - -echo "Sync complete" -echo $version > version/version diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml new file mode 100644 index 000000000000..2392595349e9 --- /dev/null +++ b/ci/tasks/build-ci-image.yml @@ -0,0 +1,31 @@ +--- +platform: linux +image_resource: + type: registry-image + source: + repository: concourse/oci-build-task + tag: 0.9.1 + registry_mirror: + host: ((registry-mirror-host)) + username: ((registry-mirror-username)) + password: ((registry-mirror-password)) +inputs: + - name: ci-images-git-repo +outputs: + - name: image +caches: + - path: ci-image-cache +params: + CONTEXT: ci-images-git-repo/ci/images + DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile + DOCKER_HUB_AUTH: ((docker-hub-auth)) +run: + path: /bin/sh + args: + - "-c" + - | + mkdir -p /root/.docker + cat > /root/.docker/config.json < Date: Thu, 5 May 2022 05:16:14 -0700 Subject: [PATCH 43/64] Remove Log4J initialization from package-info.java Closes gh-28420 --- .../main/java/org/springframework/web/util/package-info.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/package-info.java b/spring-web/src/main/java/org/springframework/web/util/package-info.java index 1f9c37a7a52d..d5cd93f8851e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/package-info.java +++ b/spring-web/src/main/java/org/springframework/web/util/package-info.java @@ -1,6 +1,5 @@ /** - * Miscellaneous web utility classes, such as HTML escaping, - * Log4j initialization, and cookie handling. + * Miscellaneous web utility classes, such as HTML escaping and cookie handling. */ @NonNullApi @NonNullFields From cf30327740aae7700ad26c80bc8d73d667eeb6d4 Mon Sep 17 00:00:00 2001 From: neals Date: Tue, 3 May 2022 20:22:09 -0700 Subject: [PATCH 44/64] Remove Log4J configurer from package-info.java in spring-core Closes gh-28411 --- .../src/main/java/org/springframework/util/package-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java index 93237c0315e6..9b64c944f83b 100644 --- a/spring-core/src/main/java/org/springframework/util/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/package-info.java @@ -1,6 +1,6 @@ /** * Miscellaneous utility classes, such as String manipulation utilities, - * a Log4J configurer, and a state holder for paged lists of objects. + * and a state holder for paged lists of objects. */ @NonNullApi @NonNullFields From ab71ff93bbd343693eee13cd7fc7ced15d0d29c1 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 5 May 2022 16:21:57 +0200 Subject: [PATCH 45/64] Polish package-info for org.springframework.util See gh-28411 --- .../src/main/java/org/springframework/util/package-info.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java index 9b64c944f83b..1c57d89eed18 100644 --- a/spring-core/src/main/java/org/springframework/util/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/package-info.java @@ -1,6 +1,6 @@ /** - * Miscellaneous utility classes, such as String manipulation utilities, - * and a state holder for paged lists of objects. + * Miscellaneous utility classes, such as utilities for working with strings, + * classes, collections, reflection, etc. */ @NonNullApi @NonNullFields From 7a75b945565261f85e059abc74cba1e115bc1e20 Mon Sep 17 00:00:00 2001 From: "Lee, Kyutae" Date: Sat, 23 Apr 2022 16:53:44 +0900 Subject: [PATCH 46/64] Fix typo in reference docs regarding RequestMappingHandlerAdapter Closes gh-28370 --- src/docs/asciidoc/integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index 3cee5f9c9ee6..c8bfe6f0b729 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -218,7 +218,7 @@ on the server side (for example, in Spring MVC REST controllers). Concrete implementations for the main media (MIME) types are provided in the framework and are, by default, registered with the `RestTemplate` on the client side and with -`RequestMethodHandlerAdapter` on the server side (see +`RequestMappingHandlerAdapter` on the server side (see <>). The implementations of `HttpMessageConverter` are described in the following sections. From 28742171fd9d8e1abe2f0a9c005a8882364c6a71 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 May 2022 18:01:31 +0200 Subject: [PATCH 47/64] Upgrade to Netty 4.1.76, Mockito 4.5.1, HtmlUnit 2.61 --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index dc4cc1da1dcb..e0b30ffea662 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6" - mavenBom "io.netty:netty-bom:4.1.75.Final" + mavenBom "io.netty:netty-bom:4.1.76.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.18" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" mavenBom "io.rsocket:rsocket-bom:1.1.2" @@ -198,7 +198,7 @@ configure(allprojects) { project -> exclude group: "org.hamcrest", name: "hamcrest-core" } } - dependencySet(group: 'org.mockito', version: '4.4.0') { + dependencySet(group: 'org.mockito', version: '4.5.1') { entry('mockito-core') { exclude group: "org.hamcrest", name: "hamcrest-core" } @@ -206,10 +206,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.12.1" - dependency("net.sourceforge.htmlunit:htmlunit:2.60.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.61.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.60.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.61.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { From b55eee1b0d9e08ab65174b1a3511cb49bf5bbcea Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 May 2022 18:01:53 +0200 Subject: [PATCH 48/64] Upgrade to ASM 9.3 Closes gh-28390 --- .../asm/AnnotationVisitor.java | 4 +-- .../org/springframework/asm/ByteVector.java | 9 +++++ .../org/springframework/asm/ClassVisitor.java | 4 +-- .../org/springframework/asm/FieldVisitor.java | 4 +-- .../springframework/asm/MethodVisitor.java | 12 +++---- .../org/springframework/asm/MethodWriter.java | 35 ++++++++++--------- .../springframework/asm/ModuleVisitor.java | 4 +-- .../asm/RecordComponentVisitor.java | 4 +-- .../java/org/springframework/asm/Type.java | 2 +- 9 files changed, 44 insertions(+), 34 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java index c3c236aefabb..05b4f9ba2f6b 100644 --- a/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java @@ -55,7 +55,7 @@ public abstract class AnnotationVisitor { * @param api the ASM API version implemented by this visitor. Must be one of the {@code * ASM}x values in {@link Opcodes}. */ - public AnnotationVisitor(final int api) { + protected AnnotationVisitor(final int api) { this(api, null); } @@ -67,7 +67,7 @@ public AnnotationVisitor(final int api) { * @param annotationVisitor the annotation visitor to which this visitor must delegate method * calls. May be {@literal null}. */ - public AnnotationVisitor(final int api, final AnnotationVisitor annotationVisitor) { + protected AnnotationVisitor(final int api, final AnnotationVisitor annotationVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 && api != Opcodes.ASM7 diff --git a/spring-core/src/main/java/org/springframework/asm/ByteVector.java b/spring-core/src/main/java/org/springframework/asm/ByteVector.java index 6187c1e22c7d..1c4b9f7e3c68 100644 --- a/spring-core/src/main/java/org/springframework/asm/ByteVector.java +++ b/spring-core/src/main/java/org/springframework/asm/ByteVector.java @@ -65,6 +65,15 @@ public ByteVector(final int initialCapacity) { this.length = data.length; } + /** + * Returns the actual number of bytes in this vector. + * + * @return the actual number of bytes in this vector. + */ + public int size() { + return length; + } + /** * Puts a byte into this byte vector. The byte vector is automatically enlarged if necessary. * diff --git a/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java index 05b448321afc..14064e742e1e 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java @@ -54,7 +54,7 @@ public abstract class ClassVisitor { * @param api the ASM API version implemented by this visitor. Must be one of the {@code * ASM}x values in {@link Opcodes}. */ - public ClassVisitor(final int api) { + protected ClassVisitor(final int api) { this(api, null); } @@ -66,7 +66,7 @@ public ClassVisitor(final int api) { * @param classVisitor the class visitor to which this visitor must delegate method calls. May be * null. */ - public ClassVisitor(final int api, final ClassVisitor classVisitor) { + protected ClassVisitor(final int api, final ClassVisitor classVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 && api != Opcodes.ASM7 diff --git a/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java index 727417045faa..2aa16bc8e619 100644 --- a/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java @@ -51,7 +51,7 @@ public abstract class FieldVisitor { * @param api the ASM API version implemented by this visitor. Must be one of the {@code * ASM}x values in {@link Opcodes}. */ - public FieldVisitor(final int api) { + protected FieldVisitor(final int api) { this(api, null); } @@ -63,7 +63,7 @@ public FieldVisitor(final int api) { * @param fieldVisitor the field visitor to which this visitor must delegate method calls. May be * null. */ - public FieldVisitor(final int api, final FieldVisitor fieldVisitor) { + protected FieldVisitor(final int api, final FieldVisitor fieldVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 && api != Opcodes.ASM7 diff --git a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java index 15ca4a5e4eb3..35ecb14c4e15 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java @@ -67,7 +67,7 @@ public abstract class MethodVisitor { * @param api the ASM API version implemented by this visitor. Must be one of the {@code * ASM}x values in {@link Opcodes}. */ - public MethodVisitor(final int api) { + protected MethodVisitor(final int api) { this(api, null); } @@ -79,7 +79,7 @@ public MethodVisitor(final int api) { * @param methodVisitor the method visitor to which this visitor must delegate method calls. May * be null. */ - public MethodVisitor(final int api, final MethodVisitor methodVisitor) { + protected MethodVisitor(final int api, final MethodVisitor methodVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 && api != Opcodes.ASM7 @@ -349,12 +349,12 @@ public void visitIntInsn(final int opcode, final int operand) { * * @param opcode the opcode of the local variable instruction to be visited. This opcode is either * ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE or RET. - * @param var the operand of the instruction to be visited. This operand is the index of a local - * variable. + * @param varIndex the operand of the instruction to be visited. This operand is the index of a + * local variable. */ - public void visitVarInsn(final int opcode, final int var) { + public void visitVarInsn(final int opcode, final int varIndex) { if (mv != null) { - mv.visitVarInsn(opcode, var); + mv.visitVarInsn(opcode, varIndex); } } diff --git a/spring-core/src/main/java/org/springframework/asm/MethodWriter.java b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java index 54f9b1c4a5dd..58fa599721f2 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodWriter.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java @@ -466,7 +466,8 @@ final class MethodWriter extends MethodVisitor { /** * Indicates what must be computed. Must be one of {@link #COMPUTE_ALL_FRAMES}, {@link - * #COMPUTE_INSERTED_FRAMES}, {@link #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_NOTHING}. + * #COMPUTE_INSERTED_FRAMES}, {@link COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link + * #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_NOTHING}. */ private final int compute; @@ -904,26 +905,26 @@ public void visitIntInsn(final int opcode, final int operand) { } @Override - public void visitVarInsn(final int opcode, final int var) { + public void visitVarInsn(final int opcode, final int varIndex) { lastBytecodeOffset = code.length; // Add the instruction to the bytecode of the method. - if (var < 4 && opcode != Opcodes.RET) { + if (varIndex < 4 && opcode != Opcodes.RET) { int optimizedOpcode; if (opcode < Opcodes.ISTORE) { - optimizedOpcode = Constants.ILOAD_0 + ((opcode - Opcodes.ILOAD) << 2) + var; + optimizedOpcode = Constants.ILOAD_0 + ((opcode - Opcodes.ILOAD) << 2) + varIndex; } else { - optimizedOpcode = Constants.ISTORE_0 + ((opcode - Opcodes.ISTORE) << 2) + var; + optimizedOpcode = Constants.ISTORE_0 + ((opcode - Opcodes.ISTORE) << 2) + varIndex; } code.putByte(optimizedOpcode); - } else if (var >= 256) { - code.putByte(Constants.WIDE).put12(opcode, var); + } else if (varIndex >= 256) { + code.putByte(Constants.WIDE).put12(opcode, varIndex); } else { - code.put11(opcode, var); + code.put11(opcode, varIndex); } // If needed, update the maximum stack size and number of locals, and stack map frames. if (currentBasicBlock != null) { if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { - currentBasicBlock.frame.execute(opcode, var, null, null); + currentBasicBlock.frame.execute(opcode, varIndex, null, null); } else { if (opcode == Opcodes.RET) { // No stack size delta. @@ -945,9 +946,9 @@ public void visitVarInsn(final int opcode, final int var) { || opcode == Opcodes.DLOAD || opcode == Opcodes.LSTORE || opcode == Opcodes.DSTORE) { - currentMaxLocals = var + 2; + currentMaxLocals = varIndex + 2; } else { - currentMaxLocals = var + 1; + currentMaxLocals = varIndex + 1; } if (currentMaxLocals > maxLocals) { maxLocals = currentMaxLocals; @@ -1307,21 +1308,21 @@ public void visitLdcInsn(final Object value) { } @Override - public void visitIincInsn(final int var, final int increment) { + public void visitIincInsn(final int varIndex, final int increment) { lastBytecodeOffset = code.length; // Add the instruction to the bytecode of the method. - if ((var > 255) || (increment > 127) || (increment < -128)) { - code.putByte(Constants.WIDE).put12(Opcodes.IINC, var).putShort(increment); + if ((varIndex > 255) || (increment > 127) || (increment < -128)) { + code.putByte(Constants.WIDE).put12(Opcodes.IINC, varIndex).putShort(increment); } else { - code.putByte(Opcodes.IINC).put11(var, increment); + code.putByte(Opcodes.IINC).put11(varIndex, increment); } // If needed, update the maximum stack size and number of locals, and stack map frames. if (currentBasicBlock != null && (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES)) { - currentBasicBlock.frame.execute(Opcodes.IINC, var, null, null); + currentBasicBlock.frame.execute(Opcodes.IINC, varIndex, null, null); } if (compute != COMPUTE_NOTHING) { - int currentMaxLocals = var + 1; + int currentMaxLocals = varIndex + 1; if (currentMaxLocals > maxLocals) { maxLocals = currentMaxLocals; } diff --git a/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java b/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java index bd2e9445b222..276035481bc9 100644 --- a/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java @@ -53,7 +53,7 @@ public abstract class ModuleVisitor { * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM6} * or {@link Opcodes#ASM7}. */ - public ModuleVisitor(final int api) { + protected ModuleVisitor(final int api) { this(api, null); } @@ -65,7 +65,7 @@ public ModuleVisitor(final int api) { * @param moduleVisitor the module visitor to which this visitor must delegate method calls. May * be null. */ - public ModuleVisitor(final int api, final ModuleVisitor moduleVisitor) { + protected ModuleVisitor(final int api, final ModuleVisitor moduleVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 && api != Opcodes.ASM7 diff --git a/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java index a66043c4d11b..6e767b6e7078 100644 --- a/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java @@ -53,7 +53,7 @@ public abstract class RecordComponentVisitor { * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM8} * or {@link Opcodes#ASM9}. */ - public RecordComponentVisitor(final int api) { + protected RecordComponentVisitor(final int api) { this(api, null); } @@ -64,7 +64,7 @@ public RecordComponentVisitor(final int api) { * @param recordComponentVisitor the record component visitor to which this visitor must delegate * method calls. May be null. */ - public RecordComponentVisitor( + protected RecordComponentVisitor( final int api, final RecordComponentVisitor recordComponentVisitor) { if (api != Opcodes.ASM9 && api != Opcodes.ASM8 diff --git a/spring-core/src/main/java/org/springframework/asm/Type.java b/spring-core/src/main/java/org/springframework/asm/Type.java index 5850ffdeb695..f346c6a44b6e 100644 --- a/spring-core/src/main/java/org/springframework/asm/Type.java +++ b/spring-core/src/main/java/org/springframework/asm/Type.java @@ -440,7 +440,7 @@ private static Type getTypeInternal( case '(': return new Type(METHOD, descriptorBuffer, descriptorBegin, descriptorEnd); default: - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Invalid descriptor: " + descriptorBuffer); } } From f963fc5f98f7c08788d35d6a3e2a75b78ae4e1be Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 May 2022 18:02:27 +0200 Subject: [PATCH 49/64] Check that nullable annotations are from org.springframework.lang Closes gh-28410 --- src/checkstyle/checkstyle-suppressions.xml | 148 +++++++++-------- src/checkstyle/checkstyle.xml | 180 ++++++++++----------- 2 files changed, 167 insertions(+), 161 deletions(-) diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 6e27de7f0729..b24ed59d9cbb 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -3,114 +3,120 @@ - - - + + + - + - + + + + - - - - - + + + + + - + - - - - - - + + + + + + + + + - - - + + + - + - + - - - - - + + + + + - - - - - - - + + + + + + + - - + + - - - + + + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - + - - - - - - + + + + + + - - - - - + + + + + diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 3ea64aef027e..a8764a5474b9 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -8,7 +8,7 @@ - + @@ -19,125 +19,125 @@ - + - - + + + value="false"/> - + - + - + - - + + - - - + + + - + - - + + - - + + + value="Class name ''{0}'' must not end with ''Test'' (checked pattern ''{1}'')."/> - - - - - - + + + + + + - + - + - + - + - + - + - - + + - + - + - - + + - + - - - - - + + + + + - + + value="^reactor\.core\.support\.Assert,^org\.slf4j\.LoggerFactory,^(?!org\.springframework).*(NonNull|Nonnull|NonNullApi|NonNullFields|Nullable)$"/> - - + + - + + value="^org\.junit\.(Test|BeforeClass|AfterClass|Before|After|Ignore|FixMethodOrder|Rule|ClassRule|Assert|Assume)$,^org\.junit\.(Assert|Assume)\..+,^org\.junit\.(experimental|internal|matchers|rules|runner|runners|validator)\..+"/> - - + + - - + + - - + + - - + + @@ -155,7 +155,7 @@ - + @@ -172,74 +172,74 @@ - - - + + + - + - + value="Line has leading space characters; indentation should be performed with tabs only."/> + - - - + + + - + + value="assertThatExceptionOfType\((NullPointerException|IllegalArgumentException|IOException|IllegalStateException)\.class\)"/> - + value="Please use specialized AssertJ assertThat*Exception method."/> + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - + - - - + + + From e441832e99f060c65f99b98af3c6c82235817db6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 May 2022 18:04:13 +0200 Subject: [PATCH 50/64] Accept WritableResource as required dependency type as well Closes gh-15284 --- .../beans/support/ResourceEditorRegistrar.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java index 2865bea12e95..784cdf1b717c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author 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 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.WritableResource; import org.springframework.core.io.support.ResourceArrayPropertyEditor; import org.springframework.core.io.support.ResourcePatternResolver; @@ -102,6 +103,7 @@ public void registerCustomEditors(PropertyEditorRegistry registry) { ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver); doRegisterEditor(registry, Resource.class, baseEditor); doRegisterEditor(registry, ContextResource.class, baseEditor); + doRegisterEditor(registry, WritableResource.class, baseEditor); doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor)); doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor)); doRegisterEditor(registry, File.class, new FileEditor(baseEditor)); From f77160378965040d46ce4a0dd81b09bcda23feaf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 5 May 2022 18:04:54 +0200 Subject: [PATCH 51/64] Polishing --- .../context/index/processor/StereotypesProvider.java | 3 ++- .../src/main/java/org/springframework/core/io/Resource.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java index 4061ca6ac405..e25d16a3b705 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ /** * Provide the list of stereotypes that match an {@link Element}. + * *

If an element has one or more stereotypes, it is referenced in the index * of candidate components and each stereotype can be queried individually. * diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index 1995ee783e6d..708aef5550c0 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author 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,14 +99,14 @@ default boolean isFile() { /** * Return a URL handle for this resource. * @throws IOException if the resource cannot be resolved as URL, - * i.e. if the resource is not available as descriptor + * i.e. if the resource is not available as a descriptor */ URL getURL() throws IOException; /** * Return a URI handle for this resource. * @throws IOException if the resource cannot be resolved as URI, - * i.e. if the resource is not available as descriptor + * i.e. if the resource is not available as a descriptor * @since 2.5 */ URI getURI() throws IOException; From 39e38763019a2b919988b3d6e8eefb266a7f75c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Bouz=C3=B3n=20Garc=C3=ADa?= Date: Fri, 6 May 2022 00:00:02 +0200 Subject: [PATCH 52/64] Fix BindingResult error when ModelAttribute has custom name in WebFlux Closes gh-28422 --- .../ErrorsMethodArgumentResolver.java | 2 +- .../ErrorsMethodArgumentResolverTests.java | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java index 28abd49dfd93..4493f0416d9c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java @@ -86,7 +86,7 @@ private Object getErrors(MethodParameter parameter, BindingContext context) { "Either declare the @ModelAttribute without an async wrapper type or " + "handle a WebExchangeBindException error signal through the async type."); - ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); + ModelAttribute ann = attributeParam.getParameterAnnotation(ModelAttribute.class); String name = (ann != null && StringUtils.hasText(ann.value()) ? ann.value() : Conventions.getVariableNameForParameter(attributeParam)); Object errors = context.getModel().asMap().get(BindingResult.MODEL_KEY_PREFIX + name); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java index 38467b710782..44e0c7080078 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java @@ -81,6 +81,21 @@ void resolve() { assertThat(actual).isSameAs(bindingResult); } + @Test + void resolveOnBindingResultAndModelAttributeWithCustomValue() { + BindingResult bindingResult = createBindingResult(new Foo(), "custom"); + this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", bindingResult); + + ResolvableMethod testMethod = ResolvableMethod.on(getClass()) + .named("handleWithModelAttributeValue").build(); + + MethodParameter parameter = testMethod.arg(Errors.class); + Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) + .block(Duration.ofMillis(5000)); + + assertThat(actual).isSameAs(bindingResult); + } + private BindingResult createBindingResult(Foo target, String name) { DataBinder binder = this.bindingContext.createDataBinder(this.exchange, target, name); return binder.getBindingResult(); @@ -98,6 +113,21 @@ void resolveWithMono() { assertThat(actual).isSameAs(bindingResult); } + @Test + void resolveWithMonoOnBindingResultAndModelAttributeWithCustomValue() { + BindingResult bindingResult = createBindingResult(new Foo(), "custom"); + this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", Mono.just(bindingResult)); + + ResolvableMethod testMethod = ResolvableMethod.on(getClass()) + .named("handleWithModelAttributeValue").build(); + + MethodParameter parameter = testMethod.arg(Errors.class); + Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) + .block(Duration.ofMillis(5000)); + + assertThat(actual).isSameAs(bindingResult); + } + @Test void resolveWithMonoOnBindingResultAndModelAttribute() { MethodParameter parameter = this.testMethod.arg(BindingResult.class); @@ -150,4 +180,13 @@ void handle( String string) { } + @SuppressWarnings("unused") + void handleWithModelAttributeValue( + @ModelAttribute("custom") Foo foo, + Errors errors, + @ModelAttribute Mono fooMono, + BindingResult bindingResult, + Mono errorsMono, + String string) { + } } From 64c96c579df7a2a9153732881632f9f4bfb856b7 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 7 May 2022 16:14:41 +0200 Subject: [PATCH 53/64] Polish contribution See gh-28422 --- .../annotation/ErrorsMethodArgumentResolver.java | 5 +++-- .../ErrorsMethodArgumentResolverTests.java | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java index 4493f0416d9c..7f6a1dea9f5a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,8 @@ /** * Resolve {@link Errors} or {@link BindingResult} method arguments. - * An {@code Errors} argument is expected to appear immediately after the + * + *

An {@code Errors} argument is expected to appear immediately after the * model attribute in the method signature. * * @author Rossen Stoyanchev diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java index 44e0c7080078..499a5b85dab6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,12 +82,11 @@ void resolve() { } @Test - void resolveOnBindingResultAndModelAttributeWithCustomValue() { + void resolveOnBindingResultAndModelAttributeWithCustomName() { BindingResult bindingResult = createBindingResult(new Foo(), "custom"); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", bindingResult); - ResolvableMethod testMethod = ResolvableMethod.on(getClass()) - .named("handleWithModelAttributeValue").build(); + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeName").build(); MethodParameter parameter = testMethod.arg(Errors.class); Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) @@ -114,12 +113,11 @@ void resolveWithMono() { } @Test - void resolveWithMonoOnBindingResultAndModelAttributeWithCustomValue() { + void resolveWithMonoOnBindingResultAndModelAttributeWithCustomName() { BindingResult bindingResult = createBindingResult(new Foo(), "custom"); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", Mono.just(bindingResult)); - ResolvableMethod testMethod = ResolvableMethod.on(getClass()) - .named("handleWithModelAttributeValue").build(); + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeName").build(); MethodParameter parameter = testMethod.arg(Errors.class); Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) @@ -181,7 +179,7 @@ void handle( } @SuppressWarnings("unused") - void handleWithModelAttributeValue( + void handleWithCustomModelAttributeName( @ModelAttribute("custom") Foo foo, Errors errors, @ModelAttribute Mono fooMono, @@ -189,4 +187,5 @@ void handleWithModelAttributeValue( Mono errorsMono, String string) { } + } From 7dd622bdb28436f859b05956b78ed573fb5bd881 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 7 May 2022 17:14:59 +0200 Subject: [PATCH 54/64] Support name attribute in @ModelAttribute in WebFlux Prior to this commit, the `name` attribute in @ModelAttribute was not supported when using WebFlux. This is because MethodParameter was used instead of SynthesizingMethodParameter when retrieving the @ModelAttribute annotation. In other words, @AliasFor was not honored because the annotation was not synthesized. Consequently, only the `value` attribute was supported in WebFlux when specifying a custom name via @ModelAttribute. This commit fixes this by using SynthesizingMethodParameter to retrieve the @ModelAttribute annotation. Closes gh-28423 --- .../method/annotation/ErrorsMethodArgumentResolver.java | 8 +++++--- .../annotation/ErrorsMethodArgumentResolverTests.java | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java index 7f6a1dea9f5a..ad66fc802819 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java @@ -22,6 +22,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; @@ -38,6 +39,7 @@ * model attribute in the method signature. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 */ public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -79,7 +81,7 @@ private Object getErrors(MethodParameter parameter, BindingContext context) { "Errors argument must be declared immediately after a model attribute argument"); int index = parameter.getParameterIndex() - 1; - MethodParameter attributeParam = MethodParameter.forExecutable(parameter.getExecutable(), index); + MethodParameter attributeParam = SynthesizingMethodParameter.forExecutable(parameter.getExecutable(), index); ReactiveAdapter adapter = getAdapterRegistry().getAdapter(attributeParam.getParameterType()); Assert.state(adapter == null, "An @ModelAttribute and an Errors/BindingResult argument " + @@ -88,8 +90,8 @@ private Object getErrors(MethodParameter parameter, BindingContext context) { "handle a WebExchangeBindException error signal through the async type."); ModelAttribute ann = attributeParam.getParameterAnnotation(ModelAttribute.class); - String name = (ann != null && StringUtils.hasText(ann.value()) ? - ann.value() : Conventions.getVariableNameForParameter(attributeParam)); + String name = (ann != null && StringUtils.hasText(ann.name()) ? ann.name() : + Conventions.getVariableNameForParameter(attributeParam)); Object errors = context.getModel().asMap().get(BindingResult.MODEL_KEY_PREFIX + name); Assert.state(errors != null, () -> "An Errors/BindingResult argument is expected " + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java index 499a5b85dab6..504122477ef1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java @@ -180,7 +180,7 @@ void handle( @SuppressWarnings("unused") void handleWithCustomModelAttributeName( - @ModelAttribute("custom") Foo foo, + @ModelAttribute(name = "custom") Foo foo, Errors errors, @ModelAttribute Mono fooMono, BindingResult bindingResult, From a1c73803980835bdafd95929098ea943c5a355dc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 9 May 2022 15:22:22 +0200 Subject: [PATCH 55/64] Add test for `value` attribute in @ModelAttribute in WebFlux This complements the previous commit which tested only the `name` attribute. See gh-28423 --- .../ErrorsMethodArgumentResolverTests.java | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java index 504122477ef1..809351c45bc1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java @@ -70,7 +70,7 @@ void supports() { } @Test - void resolve() { + void resolveWithInferredModelAttributeName() { BindingResult bindingResult = createBindingResult(new Foo(), "foo"); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "foo", bindingResult); @@ -82,11 +82,11 @@ void resolve() { } @Test - void resolveOnBindingResultAndModelAttributeWithCustomName() { + void resolveWithCustomModelAttributeNameConfiguredViaValueAttribute() { BindingResult bindingResult = createBindingResult(new Foo(), "custom"); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", bindingResult); - ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeName").build(); + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeNameViaValueAttribute").build(); MethodParameter parameter = testMethod.arg(Errors.class); Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) @@ -95,9 +95,18 @@ void resolveOnBindingResultAndModelAttributeWithCustomName() { assertThat(actual).isSameAs(bindingResult); } - private BindingResult createBindingResult(Foo target, String name) { - DataBinder binder = this.bindingContext.createDataBinder(this.exchange, target, name); - return binder.getBindingResult(); + @Test + void resolveWithCustomModelAttributeNameConfiguredViaNameAttribute() { + BindingResult bindingResult = createBindingResult(new Foo(), "custom"); + this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", bindingResult); + + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeNameViaNameAttribute").build(); + + MethodParameter parameter = testMethod.arg(Errors.class); + Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) + .block(Duration.ofMillis(5000)); + + assertThat(actual).isSameAs(bindingResult); } @Test @@ -113,11 +122,11 @@ void resolveWithMono() { } @Test - void resolveWithMonoOnBindingResultAndModelAttributeWithCustomName() { + void resolveWithMonoAndCustomModelAttributeNameConfiguredViaValueAttribute() { BindingResult bindingResult = createBindingResult(new Foo(), "custom"); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", Mono.just(bindingResult)); - ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeName").build(); + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeNameViaValueAttribute").build(); MethodParameter parameter = testMethod.arg(Errors.class); Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) @@ -146,6 +155,11 @@ void resolveWithBindingResultNotFound() { "immediately after the @ModelAttribute argument"); } + private BindingResult createBindingResult(Foo target, String name) { + DataBinder binder = this.bindingContext.createDataBinder(this.exchange, target, name); + return binder.getBindingResult(); + } + @SuppressWarnings("unused") private static class Foo { @@ -179,13 +193,15 @@ void handle( } @SuppressWarnings("unused") - void handleWithCustomModelAttributeName( + void handleWithCustomModelAttributeNameViaValueAttribute( + @ModelAttribute("custom") Foo foo, + Errors errors) { + } + + @SuppressWarnings("unused") + void handleWithCustomModelAttributeNameViaNameAttribute( @ModelAttribute(name = "custom") Foo foo, - Errors errors, - @ModelAttribute Mono fooMono, - BindingResult bindingResult, - Mono errorsMono, - String string) { + Errors errors) { } } From e26d8839b3823fb664e9762e10c94c5989c8bc44 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 9 May 2022 19:09:06 +0200 Subject: [PATCH 56/64] Stop referring to features as Java 6/7 features where unnecessary --- .../beans/factory/serviceloader/package-info.java | 2 +- .../beans/factory/support/ConstructorResolver.java | 4 ++-- .../org/springframework/jmx/export/MBeanExporter.java | 4 ++-- .../java/org/springframework/jmx/support/JmxUtils.java | 6 +++--- .../scripting/support/StandardScriptFactory.java | 4 ++-- .../core/convert/support/StringToLocaleConverter.java | 4 ++-- .../springframework/util/DefaultPropertiesPersister.java | 6 +++--- .../main/java/org/springframework/util/StringUtils.java | 4 ++-- .../jdbc/support/SQLErrorCodeSQLExceptionTranslator.java | 8 ++++---- .../jdbc/support/SQLExceptionSubclassFactory.java | 4 ++-- .../web/servlet/i18n/CookieLocaleResolver.java | 6 +++--- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java index 4e66e56347e4..b6a97c2c7ef6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java @@ -1,5 +1,5 @@ /** - * Support package for the Java 6 ServiceLoader facility. + * Support package for the Java {@link java.util.ServiceLoader} facility. */ @NonNullApi @NonNullFields diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 36190c3b817c..dd268fc517fe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -985,7 +985,7 @@ public void storeCache(RootBeanDefinition mbd, Executable constructorOrFactoryMe /** - * Delegate for checking Java 6's {@link ConstructorProperties} annotation. + * Delegate for checking Java's {@link ConstructorProperties} annotation. */ private static class ConstructorPropertiesChecker { diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java index 98090fca6ce1..e5a8b4137045 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -762,7 +762,7 @@ protected ObjectName getObjectName(Object bean, @Nullable String beanKey) throws *

The default implementation delegates to {@link JmxUtils#isMBean}, * which checks for {@link javax.management.DynamicMBean} classes as well * as classes with corresponding "*MBean" interface (Standard MBeans) - * or corresponding "*MXBean" interface (Java 6 MXBeans). + * or corresponding "*MXBean" interface (Java MXBeans). * @param beanClass the bean class to analyze * @return whether the class qualifies as an MBean * @see org.springframework.jmx.support.JmxUtils#isMBean(Class) diff --git a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java index 26c91b1ab7e5..23b45e3f7c16 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -255,7 +255,7 @@ public static Class getClassToExpose(Class clazz) { * Determine whether the given bean class qualifies as an MBean as-is. *

This implementation checks for {@link javax.management.DynamicMBean} * classes as well as classes with corresponding "*MBean" interface - * (Standard MBeans) or corresponding "*MXBean" interface (Java 6 MXBeans). + * (Standard MBeans) or corresponding "*MXBean" interface (Java MXBeans). * @param clazz the bean class to analyze * @return whether the class qualifies as an MBean * @see org.springframework.jmx.export.MBeanExporter#isMBean(Class) @@ -289,7 +289,7 @@ public static Class getMBeanInterface(@Nullable Class clazz) { } /** - * Return the Java 6 MXBean interface exists for the given class, if any + * Return the Java MXBean interface for the given class, if any * (that is, an interface whose name ends with "MXBean" and/or * carries an appropriate MXBean annotation). * @param clazz the class to check diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java index 80a1a8fb6fac..ef76657a61a5 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ /** * {@link org.springframework.scripting.ScriptFactory} implementation based - * on the JSR-223 script engine abstraction (as included in Java 6+). + * on the JSR-223 script engine abstraction (as included in Java). * Supports JavaScript, Groovy, JRuby, and other JSR-223 compliant engines. * *

Typically used in combination with a diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java index d68fe562b635..2b5bf654b81c 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * Converts from a String to a {@link java.util.Locale}. * *

Accepts the classic {@link Locale} String format ({@link Locale#toString()}) - * as well as BCP 47 language tags ({@link Locale#forLanguageTag} on Java 7+). + * as well as BCP 47 language tags ({@link Locale#forLanguageTag}. * * @author Keith Donald * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java index a369e3366d95..d0f3bc296f7d 100644 --- a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java +++ b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author 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,8 +35,8 @@ *

Loading from and storing to a stream delegates to {@code Properties.load} * and {@code Properties.store}, respectively, to be fully compatible with * the Unicode conversion as implemented by the JDK Properties class. As of JDK 6, - * {@code Properties.load/store} will also be used for readers/writers, - * effectively turning this class into a plain backwards compatibility adapter. + * {@code Properties.load/store} is also used for readers/writers, effectively + * turning this class into a plain backwards compatibility adapter. * *

The persistence code that works with Reader/Writer follows the JDK's parsing * strategy but does not implement Unicode conversion, because the Reader/Writer diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index fd70a8acea78..ef6e215a3887 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -804,11 +804,11 @@ public static String uriDecode(String source, Charset charset) { /** * Parse the given {@code String} value into a {@link Locale}, accepting - * the {@link Locale#toString} format as well as BCP 47 language tags. + * the {@link Locale#toString} format as well as BCP 47 language tags as + * specified by {@link Locale#forLanguageTag}. * @param localeValue the locale value: following either {@code Locale's} * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as * separators (as an alternative to underscores), or BCP 47 (e.g. "en-UK") - * as specified by {@link Locale#forLanguageTag} on Java 7+ * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification * @since 5.0.4 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index f50380fcdec0..80f89796049d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author 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,9 +50,9 @@ * by default. This factory loads a "sql-error-codes.xml" file from the class path, * defining error code mappings for database names from database meta-data. *

  • Fallback to a fallback translator. {@link SQLStateSQLExceptionTranslator} is the - * default fallback translator, analyzing the exception's SQL state only. On Java 6 - * which introduces its own {@code SQLException} subclass hierarchy, we will - * use {@link SQLExceptionSubclassTranslator} by default, which in turns falls back + * default fallback translator, analyzing the exception's SQL state only. Since Java 6 + * which introduces its own {@code SQLException} subclass hierarchy, we use + * {@link SQLExceptionSubclassTranslator} by default, which in turns falls back * to Spring's own SQL state translation when not encountering specific subclasses. * * diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java index 7a6d99dd0534..a172139de1e5 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import java.sql.SQLTransientConnectionException; /** - * Class to generate Java 6 SQLException subclasses for testing purposes. + * Class to generate {@link SQLException} subclasses for testing purposes. * * @author Thomas Risberg */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java index 557bf7801e85..9d5bf3e6ca8d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -314,9 +314,9 @@ protected Locale parseLocaleValue(String localeValue) { /** * Render the given locale as a text value for inclusion in a cookie. *

    The default implementation calls {@link Locale#toString()} - * or JDK 7's {@link Locale#toLanguageTag()}, depending on the + * or {@link Locale#toLanguageTag()}, depending on the * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. - * @param locale the locale to stringify + * @param locale the locale to convert to a string * @return a String representation for the given locale * @since 4.3 * @see #isLanguageTagCompliant() From 941b92cbede8536e384d766091d625c89f1980d0 Mon Sep 17 00:00:00 2001 From: "evgeny.bovykin" Date: Mon, 9 May 2022 21:39:05 +0200 Subject: [PATCH 57/64] Make inner classes static when feasible A static nested class does not keep an implicit reference to its enclosing instance. This prevents a common cause of memory leaks and uses less memory per instance of the class. Closes gh-28433 --- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- .../web/reactive/resource/VersionResourceResolver.java | 2 +- .../socket/server/upgrade/UndertowRequestUpgradeStrategy.java | 2 +- .../web/servlet/resource/VersionResourceResolver.java | 2 +- .../sockjs/transport/handler/XhrStreamingTransportHandler.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 3364a95493db..09037ecc158c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -88,7 +88,7 @@ public void handleRequest(HttpServerExchange exchange) { } - private class HandlerResultSubscriber implements Subscriber { + private static class HandlerResultSubscriber implements Subscriber { private final HttpServerExchange exchange; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java index ff712f7068ba..12b693b4f8c7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java @@ -236,7 +236,7 @@ protected VersionStrategy getStrategyForPath(String requestPath) { } - private class FileNameVersionedResource extends AbstractResource implements HttpResource { + private static class FileNameVersionedResource extends AbstractResource implements HttpResource { private final Resource original; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java index 973bbf4fb2e9..4a0e26589fc8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java @@ -82,7 +82,7 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, } - private class DefaultCallback implements WebSocketConnectionCallback { + private static class DefaultCallback implements WebSocketConnectionCallback { private final HandshakeInfo handshakeInfo; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index d2041e5b6569..753e06fbefbd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -232,7 +232,7 @@ protected VersionStrategy getStrategyForPath(String requestPath) { } - private class FileNameVersionedResource extends AbstractResource implements HttpResource { + private static class FileNameVersionedResource extends AbstractResource implements HttpResource { private final Resource original; diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java index f947643205ce..08682e3c4871 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java @@ -75,7 +75,7 @@ protected SockJsFrameFormat getFrameFormat(ServerHttpRequest request) { } - private class XhrStreamingSockJsSession extends StreamingSockJsSession { + private static class XhrStreamingSockJsSession extends StreamingSockJsSession { public XhrStreamingSockJsSession(String sessionId, SockJsServiceConfig config, WebSocketHandler wsHandler, Map attributes) { From 1c10cdd1e87f133c082771587a730bbf2255a920 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 10 May 2022 11:32:26 +0200 Subject: [PATCH 58/64] Update copyright dates See gh-28433 --- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- .../web/reactive/resource/VersionResourceResolver.java | 2 +- .../socket/server/upgrade/UndertowRequestUpgradeStrategy.java | 2 +- .../web/servlet/resource/VersionResourceResolver.java | 2 +- .../sockjs/transport/handler/XhrStreamingTransportHandler.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 09037ecc158c..8c58eb159d8c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java index 12b693b4f8c7..446ced9e24fa 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java index 4a0e26589fc8..91e01527ee32 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index 753e06fbefbd..3183c8334984 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java index 08682e3c4871..2e8c3aea42cb 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/XhrStreamingTransportHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From de6180b093a61ad852f537d49203740fc04207ed Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 May 2022 13:05:56 +0200 Subject: [PATCH 59/64] Upgrade to Reactor 2020.0.19 Includes Netty 4.1.77 and RxJava 3.1.4. Closes gh-28437 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index e0b30ffea662..ef7fd918d2fd 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,8 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6" - mavenBom "io.netty:netty-bom:4.1.76.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.18" + mavenBom "io.netty:netty-bom:4.1.77.Final" + mavenBom "io.projectreactor:reactor-bom:2020.0.19" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" mavenBom "io.rsocket:rsocket-bom:1.1.2" mavenBom "org.eclipse.jetty:jetty-bom:9.4.46.v20220331" @@ -67,7 +67,7 @@ configure(allprojects) { project -> dependency "io.reactivex:rxjava:1.3.8" dependency "io.reactivex:rxjava-reactive-streams:1.2.1" dependency "io.reactivex.rxjava2:rxjava:2.2.21" - dependency "io.reactivex.rxjava3:rxjava:3.1.3" + dependency "io.reactivex.rxjava3:rxjava:3.1.4" dependency "io.smallrye.reactive:mutiny:1.4.0" dependency "io.projectreactor.tools:blockhound:1.0.6.RELEASE" From c81e11d537f396b9bf56bea30354ded2204861b8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 10 May 2022 13:07:00 +0200 Subject: [PATCH 60/64] Polishing --- .../SQLExceptionSubclassTranslator.java | 4 ++-- .../http/codec/multipart/DefaultParts.java | 18 ++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index 3532753e8cc5..7e86c84fce43 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ * @author Juergen Hoeller * @since 2.5 * @see java.sql.SQLTransientException - * @see java.sql.SQLTransientException + * @see java.sql.SQLNonTransientException * @see java.sql.SQLRecoverableException */ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLExceptionTranslator { diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java index 284c82497b96..8f6403f8eed4 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultParts.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,13 +100,12 @@ private static Part partInternal(HttpHeaders headers, Content content) { /** - * Abstract base class. + * Abstract base class for {@link Part} implementations. */ private static abstract class AbstractPart implements Part { private final HttpHeaders headers; - protected AbstractPart(HttpHeaders headers) { Assert.notNull(headers, "HttpHeaders is required"); this.headers = headers; @@ -119,7 +118,6 @@ public String name() { return name; } - @Override public HttpHeaders headers() { return this.headers; @@ -172,7 +170,6 @@ private static class DefaultPart extends AbstractPart { protected final Content content; - public DefaultPart(HttpHeaders headers, Content content) { super(headers); this.content = content; @@ -198,7 +195,6 @@ public String toString() { return "DefaultPart"; } } - } @@ -213,7 +209,7 @@ public DefaultFilePart(HttpHeaders headers, Content content) { @Override public String filename() { - String filename = this.headers().getContentDisposition().getFilename(); + String filename = headers().getContentDisposition().getFilename(); Assert.state(filename != null, "No filename found"); return filename; } @@ -235,7 +231,6 @@ public String toString() { return "DefaultFilePart{(" + filename + ")}"; } } - } @@ -249,9 +244,9 @@ private interface Content { Mono transferTo(Path dest); Mono delete(); - } + /** * {@code Content} implementation based on a flux of data buffers. */ @@ -259,12 +254,10 @@ private static final class FluxContent implements Content { private final Flux content; - public FluxContent(Flux content) { this.content = content; } - @Override public Flux content() { return this.content; @@ -279,7 +272,6 @@ public Mono transferTo(Path dest) { public Mono delete() { return Mono.empty(); } - } @@ -292,13 +284,11 @@ private static final class FileContent implements Content { private final Scheduler scheduler; - public FileContent(Path file, Scheduler scheduler) { this.file = file; this.scheduler = scheduler; } - @Override public Flux content() { return DataBufferUtils.readByteChannel( From e4ec376075b617c70edcaa409540698f588f4af9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 10 May 2022 15:55:00 +0200 Subject: [PATCH 61/64] Disabling Undertow server in CoroutinesIntegrationTests --- .../result/method/annotation/CoroutinesIntegrationTests.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt index 18ab6e1dda78..6ce490d677a9 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Assumptions.assumeFalse import org.springframework.context.ApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan @@ -36,6 +37,7 @@ import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.HttpServerErrorException import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer import reactor.core.publisher.Flux import java.time.Duration @@ -114,6 +116,8 @@ class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { @ParameterizedHttpServerTest fun `Suspending handler method returning ResponseEntity of Flux `(httpServer: HttpServer) { + assumeFalse(httpServer is UndertowHttpServer, "Undertow currently fails") + startServer(httpServer) val entity = performGet("/entity-flux", HttpHeaders.EMPTY, String::class.java) From dc2947c52df18d5e99cad03383f7d6ba13d031fd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 29 Apr 2022 10:41:16 +0100 Subject: [PATCH 62/64] Ignore invalid connect frame Closes gh-28443 --- .../broker/SimpleBrokerMessageHandler.java | 8 +++++- .../stomp/StompBrokerRelayMessageHandler.java | 8 +++++- .../StompBrokerRelayMessageHandlerTests.java | 26 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java index bcfe2dc3fe61..7f795eb67e30 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -306,6 +306,12 @@ protected void handleMessageInternal(Message message) { else if (SimpMessageType.CONNECT.equals(messageType)) { logMessage(message); if (sessionId != null) { + if (this.sessions.get(sessionId) != null) { + if (logger.isWarnEnabled()) { + logger.warn("Ignoring CONNECT in session " + sessionId + ". Already connected."); + } + return; + } long[] heartbeatIn = SimpMessageHeaderAccessor.getHeartbeat(headers); long[] heartbeatOut = getHeartbeatValue(); Principal user = SimpMessageHeaderAccessor.getUser(headers); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java index f756b0b1f9ea..a0285a2d2f5b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -552,6 +552,12 @@ else if (accessor instanceof SimpMessageHeaderAccessor) { } if (StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command)) { + if (this.connectionHandlers.get(sessionId) != null) { + if (logger.isWarnEnabled()) { + logger.warn("Ignoring CONNECT in session " + sessionId + ". Already connected."); + } + return; + } if (logger.isDebugEnabled()) { logger.debug(stompAccessor.getShortLogMessage(EMPTY_PAYLOAD)); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java index 3fc34442d7fd..44f52a68b4e5 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -260,6 +260,30 @@ void systemSubscription() { assertThat(captor.getValue()).isSameAs(message); } + @Test + void alreadyConnected() { + + this.brokerRelay.start(); + + Message connect = connectMessage("sess1", "joe"); + this.brokerRelay.handleMessage(connect); + + assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(2); + + StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0); + assertThat(headers1.getCommand()).isEqualTo(StompCommand.CONNECT); + assertThat(headers1.getSessionId()).isEqualTo(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID); + + StompHeaderAccessor headers2 = this.tcpClient.getSentHeaders(1); + assertThat(headers2.getCommand()).isEqualTo(StompCommand.CONNECT); + assertThat(headers2.getSessionId()).isEqualTo("sess1"); + + this.brokerRelay.handleMessage(connect); + + assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(2); + assertThat(this.outboundChannel.getMessages()).isEmpty(); + } + private Message connectMessage(String sessionId, String user) { StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT); headers.setSessionId(sessionId); From 83186b689f11f5e6efe7ccc08fdeb92f66fcd583 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 11 May 2022 08:32:06 +0200 Subject: [PATCH 63/64] Refine CachedIntrospectionResults property introspection Closes gh-28445 --- .../beans/CachedIntrospectionResults.java | 26 ++++++++++++------- .../beans/BeanWrapperTests.java | 21 ++++++++++++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 8332045197de..bd234eb58f59 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -22,6 +22,7 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URL; import java.security.ProtectionDomain; import java.util.Collections; import java.util.HashSet; @@ -292,10 +293,12 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { // Only allow all name variants of Class properties continue; } - if (pd.getWriteMethod() == null && pd.getPropertyType() != null && - (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) || - ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) { - // Ignore ClassLoader and ProtectionDomain read-only properties - no need to bind to those + if (URL.class == beanClass && "content".equals(pd.getName())) { + // Only allow URL attribute introspection, not content resolution + continue; + } + if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType())) { + // Ignore read-only properties such as ClassLoader - no need to bind to those continue; } if (logger.isTraceEnabled()) { @@ -344,10 +347,8 @@ private void introspectInterfaces(Class beanClass, Class currClass, Set 0 || method.getReturnType() == void.class || - ClassLoader.class.isAssignableFrom(method.getReturnType()) || - ProtectionDomain.class.isAssignableFrom(method.getReturnType())) { + isInvalidReadOnlyPropertyType(method.getReturnType())) { return false; } try { @@ -393,6 +393,12 @@ private boolean isPlainAccessor(Method method) { } } + private boolean isInvalidReadOnlyPropertyType(@Nullable Class returnType) { + return (returnType != null && (AutoCloseable.class.isAssignableFrom(returnType) || + ClassLoader.class.isAssignableFrom(returnType) || + ProtectionDomain.class.isAssignableFrom(returnType))); + } + BeanInfo getBeanInfo() { return this.beanInfo; diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index ab154ea3c4e6..dd79e71cb13f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -25,6 +25,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.OverridingClassLoader; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.UrlResource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -153,7 +154,7 @@ void setPropertyTypeMismatch() { } @Test - void propertyDescriptors() { + void propertyDescriptors() throws Exception { TestBean target = new TestBean(); target.setSpouse(new TestBean()); BeanWrapper accessor = createAccessor(target); @@ -182,11 +183,29 @@ void propertyDescriptors() { assertThat(accessor.isReadableProperty("class.package")).isFalse(); assertThat(accessor.isReadableProperty("class.module")).isFalse(); assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); assertThat(accessor.isReadableProperty("classLoader")).isTrue(); assertThat(accessor.isWritableProperty("classLoader")).isTrue(); OverridingClassLoader ocl = new OverridingClassLoader(getClass().getClassLoader()); accessor.setPropertyValue("classLoader", ocl); assertThat(accessor.getPropertyValue("classLoader")).isSameAs(ocl); + + accessor = createAccessor(new UrlResource("https://spring.io/")); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.isReadableProperty("URL.protocol")).isTrue(); + assertThat(accessor.isReadableProperty("URL.host")).isTrue(); + assertThat(accessor.isReadableProperty("URL.port")).isTrue(); + assertThat(accessor.isReadableProperty("URL.file")).isTrue(); + assertThat(accessor.isReadableProperty("URL.content")).isFalse(); + assertThat(accessor.isReadableProperty("inputStream")).isFalse(); + assertThat(accessor.isReadableProperty("filename")).isTrue(); + assertThat(accessor.isReadableProperty("description")).isTrue(); } @Test From e0f56e7d80a4e1248198e40be99157dbd8f594af Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Wed, 11 May 2022 06:55:19 +0000 Subject: [PATCH 64/64] Release v5.3.20 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a03487da791a..83e2a7260bc6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.20-SNAPSHOT +version=5.3.20 org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true