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 diff --git a/build.gradle b/build.gradle index d21c458e1fcb..ef7fd918d2fd 100644 --- a/build.gradle +++ b/build.gradle @@ -28,11 +28,11 @@ 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.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.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" 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" @@ -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" @@ -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") { @@ -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.59.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.61.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.59.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") { @@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "9.3" + toolVersion = "10.1" configDirectory.set(rootProject.file("src/checkstyle")) } diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index ac4db911a85f..2e2c9b9a7d60 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal-20220302 +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 d090494f6627..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/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.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz" ;; *) echo $"Unknown java version" 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..2f970570a230 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" @@ -7,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 eb8de81ea1e1..5f7e91ba4661 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)) @@ -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: @@ -41,6 +45,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: @@ -87,7 +96,7 @@ resources: branch: ((branch)) paths: ["ci/images/*"] - name: ci-image - type: docker-image + type: registry-image icon: docker source: <<: *docker-resource-source @@ -157,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 @@ -340,7 +356,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 @@ -385,7 +400,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 @@ -430,7 +444,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/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 <[] 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/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... 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/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-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-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index 4187097ce371..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; @@ -287,13 +288,17 @@ 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 (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()) { @@ -342,9 +347,8 @@ private void introspectInterfaces(Class beanClass, Class currClass, Set 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()); @@ -373,8 +377,10 @@ 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 || + isInvalidReadOnlyPropertyType(method.getReturnType())) { return false; } try { @@ -387,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/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-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-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)); 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..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. @@ -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 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 8856e31a0e05..dd79e71cb13f 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.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. @@ -23,6 +23,9 @@ import org.junit.jupiter.api.Test; 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; @@ -105,7 +108,7 @@ void checkNotWritablePropertyHoldPossibleMatches() { .satisfies(ex -> 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(); @@ -151,18 +154,58 @@ void setPropertyTypeMismatch() { } @Test - void propertyDescriptors() { + void propertyDescriptors() throws Exception { TestBean target = new TestBean(); target.setSpouse(new TestBean()); 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("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 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-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/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-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/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/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 { +} 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-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; } } 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..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 @@ -5,19 +5,19 @@ - + - + - - + + - + \ No newline at end of file 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); } } 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-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/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; 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-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/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/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 05809bc5ad7b..ed4919e87bc0 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -65,9 +65,22 @@ public abstract class MimeTypeUtils { */ public static final String ALL_VALUE = "*/*"; + /** + * 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; /** @@ -165,6 +178,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-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-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java index 93237c0315e6..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, - * a Log4J configurer, 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 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"; + } + } + } 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-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); } } 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/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/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-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; } } 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-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-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); 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..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 @@ -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,13 +32,17 @@ 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.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -56,19 +60,24 @@ /** * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Jaebin Joo */ 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"))); @@ -225,19 +234,14 @@ public void multipartRequestWithOptionalFileListNotPresent() throws Exception { } @Test - public void multipartRequestWithServletParts() 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("/multipartfile").part(filePart).part(jsonPart)) + .perform(multipart("/multipartfilebinding").part(filePart)) .andExpect(status().isFound()) - .andExpect(model().attribute("fileContent", fileContent)) - .andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah"))); + .andExpect(model().attribute("fileContent", fileContent)); } @Test // SPR-13317 @@ -343,10 +347,13 @@ public String processOptionalFileList(@RequestParam Optional } @RequestMapping(value = "/part", method = RequestMethod.POST) - public String processPart(@RequestParam Part part, + public String processPart(@RequestPart Part part, @RequestPart Map json, Model model) throws IOException { - model.addAttribute("fileContent", part.getInputStream()); + if (part != null) { + byte[] content = StreamUtils.copyToByteArray(part.getInputStream()); + model.addAttribute("fileContent", content); + } model.addAttribute("jsonContent", json); return "redirect:/index"; @@ -357,6 +364,33 @@ public String processMultipart(@RequestPart Map json, Model mode model.addAttribute("json", json); return "redirect:/index"; } + + @RequestMapping(value = "/multipartfilebinding", 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"; + } + } + + private static class MultipartFileBean { + + private MultipartFile file; + + public MultipartFile getFile() { + return file; + } + + @SuppressWarnings("unused") + public void setFile(MultipartFile file) { + this.file = file; + } } 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..c30efd4c3f0e 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,19 @@ 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}. + * @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"; + /** * Public constant media type for {@code application/json}. */ @@ -396,6 +409,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"); 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( 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 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 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..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 @@ -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. @@ -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 @@ -236,10 +235,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 +253,7 @@ DataBuffer readFromInputStream() throws IOException { return EOF_BUFFER; } - return null; + return AbstractListenerReadPublisher.EMPTY_BUFFER; } protected final void logBytesRead(int read) { 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 { 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..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. @@ -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-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 16bf30ee01fc..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 @@ -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. @@ -95,11 +104,13 @@ 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; - * 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 * @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/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..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 @@ -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. @@ -98,9 +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 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 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 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 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); 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-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-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..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. @@ -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/result/method/annotation/ErrorsMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolver.java index 28abd49dfd93..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 @@ -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. @@ -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; @@ -33,10 +34,12 @@ /** * 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 + * @author Sam Brannen * @since 5.0 */ public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { @@ -78,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 " + @@ -86,9 +89,9 @@ 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); - String name = (ann != null && StringUtils.hasText(ann.value()) ? - ann.value() : Conventions.getVariableNameForParameter(attributeParam)); + ModelAttribute ann = attributeParam.getParameterAnnotation(ModelAttribute.class); + 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/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/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..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. @@ -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-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/ErrorsMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ErrorsMethodArgumentResolverTests.java index 38467b710782..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 @@ -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. @@ -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); @@ -81,9 +81,32 @@ void resolve() { 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 resolveWithCustomModelAttributeNameConfiguredViaValueAttribute() { + BindingResult bindingResult = createBindingResult(new Foo(), "custom"); + this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "custom", bindingResult); + + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handleWithCustomModelAttributeNameViaValueAttribute").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 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 @@ -98,6 +121,20 @@ void resolveWithMono() { assertThat(actual).isSameAs(bindingResult); } + @Test + 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("handleWithCustomModelAttributeNameViaValueAttribute").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); @@ -118,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 { @@ -150,4 +192,16 @@ void handle( String string) { } + @SuppressWarnings("unused") + void handleWithCustomModelAttributeNameViaValueAttribute( + @ModelAttribute("custom") Foo foo, + Errors errors) { + } + + @SuppressWarnings("unused") + void handleWithCustomModelAttributeNameViaNameAttribute( + @ModelAttribute(name = "custom") Foo foo, + Errors errors) { + } + } 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-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/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") 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..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 @@ -24,7 +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.Disabled +import org.junit.jupiter.api.Assumptions.assumeFalse import org.springframework.context.ApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan @@ -37,10 +37,10 @@ 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 -@Disabled class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { override fun initApplicationContext(): ApplicationContext { @@ -116,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) 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() 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; } 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/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..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. @@ -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 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..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. @@ -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-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 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..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. @@ -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) { 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."/> + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - + - - - + + + 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. 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. 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]#<>#