diff --git a/build.gradle b/build.gradle index d21c458e1fcb..dc4cc1da1dcb 100644 --- a/build.gradle +++ b/build.gradle @@ -29,10 +29,10 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6" mavenBom "io.netty:netty-bom:4.1.75.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.17" - mavenBom "io.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.projectreactor:reactor-bom:2020.0.18" + mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13" + mavenBom "io.rsocket:rsocket-bom:1.1.2" + mavenBom "org.eclipse.jetty:jetty-bom:9.4.46.v20220331" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2" mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2" @@ -96,7 +96,7 @@ configure(allprojects) { project -> dependency "com.h2database:h2:2.1.210" dependency "com.github.ben-manes.caffeine:caffeine:2.9.3" - dependency "com.github.librepdf:openpdf:1.3.26" + dependency "com.github.librepdf:openpdf:1.3.27" dependency "com.rometools:rome:1.18.0" dependency "commons-io:commons-io:2.5" dependency "io.vavr:vavr:0.10.4" @@ -128,18 +128,18 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.48" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.60') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.62') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.60') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.62') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } - dependencySet(group: 'io.undertow', version: '2.2.16.Final') { + dependencySet(group: 'io.undertow', version: '2.2.17.Final') { entry 'undertow-core' entry('undertow-servlet') { exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec" @@ -150,7 +150,7 @@ configure(allprojects) { project -> } } - dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.10" + dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.11" dependency 'org.apache.httpcomponents.client5:httpclient5:5.1.3' dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.1.3' dependency("org.apache.httpcomponents:httpclient:4.5.13") { @@ -206,10 +206,10 @@ configure(allprojects) { project -> } dependency "io.mockk:mockk:1.12.1" - dependency("net.sourceforge.htmlunit:htmlunit:2.59.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.60.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.59.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.60.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "9.3" + toolVersion = "10.1" configDirectory.set(rootProject.file("src/checkstyle")) } diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index ac4db911a85f..37ab9b6edb47 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal-20220302 +FROM ubuntu:focal-20220404 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index d090494f6627..3b0006dd7f6d 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -12,7 +12,7 @@ case "$1" in echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz" ;; java18) - echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk18-2022-02-12-08-06-beta/OpenJDK18-jdk_x64_linux_hotspot_2022-02-12-08-06.tar.gz" + echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz" ;; *) echo $"Unknown java version" diff --git a/ci/pipeline.yml b/ci/pipeline.yml index eb8de81ea1e1..712bd2cac0de 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -41,6 +41,11 @@ anchors: GITHUB_TOKEN: ((github-ci-release-token)) resource_types: +- name: registry-image + type: registry-image + source: + repository: concourse/registry-image-resource + tag: 1.5.0 - name: artifactory-resource type: registry-image source: diff --git a/gradle.properties b/gradle.properties index 75c9b894bcfa..00985a0fbeed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.18-SNAPSHOT +version=5.3.19 org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index f6f7bd7acfd6..08274272386a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -134,7 +134,7 @@ static Class[] completeProxiedInterfaces(AdvisedSupport advised, boolean deco if (targetClass.isInterface()) { advised.setInterfaces(targetClass); } - else if (Proxy.isProxyClass(targetClass) || isLambda(targetClass)) { + else if (Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { advised.setInterfaces(targetClass.getInterfaces()); } specifiedInterfaces = advised.getProxiedInterfaces(); @@ -245,18 +245,4 @@ static Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] argu return arguments; } - /** - * Determine if the supplied {@link Class} is a JVM-generated implementation - * class for a lambda expression or method reference. - *

This method makes a best-effort attempt at determining this, based on - * checks that work on modern, main stream JVMs. - * @param clazz the class to check - * @return {@code true} if the class is a lambda implementation class - * @since 5.3.16 - */ - static boolean isLambda(Class clazz) { - return (clazz.isSynthetic() && (clazz.getSuperclass() == Object.class) && - (clazz.getInterfaces().length > 0) && clazz.getName().contains("$$Lambda")); - } - } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/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..8332045197de 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -287,13 +287,15 @@ private CachedIntrospectionResults(Class beanClass) throws BeansException { // This call is slow so we do it once. PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { - if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) { + if (Class.class == beanClass && !("name".equals(pd.getName()) || + (pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) { // Only allow all name variants of Class properties continue; } - if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) - || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) { - // Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those + if (pd.getWriteMethod() == null && pd.getPropertyType() != null && + (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) || + ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) { + // Ignore ClassLoader and ProtectionDomain read-only properties - no need to bind to those continue; } if (logger.isTraceEnabled()) { @@ -342,9 +344,10 @@ private void introspectInterfaces(Class beanClass, Class currClass, Set 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 +376,11 @@ private void introspectPlainAccessors(Class beanClass, Set readMethod } private boolean isPlainAccessor(Method method) { - if (method.getParameterCount() > 0 || method.getReturnType() == void.class || - method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) { + if (Modifier.isStatic(method.getModifiers()) || + method.getDeclaringClass() == Object.class || method.getDeclaringClass() == Class.class || + method.getParameterCount() > 0 || method.getReturnType() == void.class || + ClassLoader.class.isAssignableFrom(method.getReturnType()) || + ProtectionDomain.class.isAssignableFrom(method.getReturnType())) { return false; } try { 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/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java index 8856e31a0e05..ab154ea3c4e6 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,8 @@ 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -105,7 +107,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(); @@ -157,12 +159,34 @@ void propertyDescriptors() { BeanWrapper accessor = createAccessor(target); accessor.setPropertyValue("name", "a"); accessor.setPropertyValue("spouse.name", "b"); + assertThat(target.getName()).isEqualTo("a"); assertThat(target.getSpouse().getName()).isEqualTo("b"); assertThat(accessor.getPropertyValue("name")).isEqualTo("a"); assertThat(accessor.getPropertyValue("spouse.name")).isEqualTo("b"); assertThat(accessor.getPropertyDescriptor("name").getPropertyType()).isEqualTo(String.class); assertThat(accessor.getPropertyDescriptor("spouse.name").getPropertyType()).isEqualTo(String.class); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("class.name")).isTrue(); + assertThat(accessor.isReadableProperty("class.simpleName")).isTrue(); + assertThat(accessor.getPropertyValue("class.name")).isEqualTo(TestBean.class.getName()); + assertThat(accessor.getPropertyValue("class.simpleName")).isEqualTo(TestBean.class.getSimpleName()); + assertThat(accessor.getPropertyDescriptor("class.name").getPropertyType()).isEqualTo(String.class); + assertThat(accessor.getPropertyDescriptor("class.simpleName").getPropertyType()).isEqualTo(String.class); + + accessor = createAccessor(new DefaultResourceLoader()); + + assertThat(accessor.isReadableProperty("class.package")).isFalse(); + assertThat(accessor.isReadableProperty("class.module")).isFalse(); + assertThat(accessor.isReadableProperty("class.classLoader")).isFalse(); + assertThat(accessor.isReadableProperty("classLoader")).isTrue(); + assertThat(accessor.isWritableProperty("classLoader")).isTrue(); + OverridingClassLoader ocl = new OverridingClassLoader(getClass().getClassLoader()); + accessor.setPropertyValue("classLoader", ocl); + assertThat(accessor.getPropertyValue("classLoader")).isSameAs(ocl); } @Test diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java index a383ae5f8bf7..ea2fb76ae882 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; @@ -43,6 +44,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; @@ -52,18 +54,18 @@ import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; import static org.assertj.core.api.Assertions.assertThat; /** * @author Juergen Hoeller */ -@SuppressWarnings("resource") -public class ValidatorFactoryTests { +class ValidatorFactoryTests { @Test - @SuppressWarnings("cast") - public void testSimpleValidation() { + void simpleValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -78,15 +80,15 @@ public void testSimpleValidation() { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - @SuppressWarnings("cast") - public void testSimpleValidationWithCustomProvider() { + void simpleValidationWithCustomProvider() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.setProviderClass(HibernateValidator.class); validator.afterPropertiesSet(); @@ -102,14 +104,15 @@ public void testSimpleValidationWithCustomProvider() { Validator nativeValidator = validator.unwrap(Validator.class); assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); - assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); - assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); validator.destroy(); } @Test - public void testSimpleValidationWithClassLevel() { + void simpleValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -122,10 +125,13 @@ public void testSimpleValidationWithClassLevel() { ConstraintViolation cv = iterator.next(); assertThat(cv.getPropertyPath().toString()).isEqualTo(""); assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationFieldType() { + void springValidationFieldType() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -135,11 +141,16 @@ public void testSpringValidationFieldType() { BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); validator.validate(person, errors); assertThat(errors.getErrorCount()).isEqualTo(1); - assertThat(errors.getFieldError("address").getRejectedValue()).isInstanceOf(ValidAddress.class); + assertThat(errors.getFieldError("address").getRejectedValue()) + .as("Field/Value type mismatch") + .isInstanceOf(ValidAddress.class); + + validator.destroy(); } @Test - public void testSpringValidation() { + void springValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -164,10 +175,13 @@ public void testSpringValidation() { assertThat(errorCodes.contains("NotNull.street")).isTrue(); assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); assertThat(errorCodes.contains("NotNull")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithClassLevel() { + void springValidationWithClassLevel() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -182,10 +196,12 @@ public void testSpringValidationWithClassLevel() { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); } @Test - public void testSpringValidationWithAutowiredValidator() { + void springValidationWithAutowiredValidator() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( LocalValidatorFactoryBean.class); LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); @@ -202,11 +218,14 @@ public void testSpringValidationWithAutowiredValidator() { assertThat(errorCodes.size()).isEqualTo(2); assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + + validator.destroy(); ctx.close(); } @Test - public void testSpringValidationWithErrorInListElement() { + void springValidationWithErrorInListElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -221,10 +240,13 @@ public void testSpringValidationWithErrorInListElement() { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressList[0].street"); assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); + + validator.destroy(); } @Test - public void testSpringValidationWithErrorInSetElement() { + void springValidationWithErrorInSetElement() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -239,10 +261,13 @@ public void testSpringValidationWithErrorInSetElement() { assertThat(fieldError.getField()).isEqualTo("address.street"); fieldError = result.getFieldError("addressSet[].street"); assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); + + validator.destroy(); } @Test - public void testInnerBeanValidation() { + void innerBeanValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -251,10 +276,13 @@ public void testInnerBeanValidation() { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testValidationWithOptionalField() { + void validationWithOptionalField() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -263,10 +291,13 @@ public void testValidationWithOptionalField() { validator.validate(mainBean, errors); Object rejected = errors.getFieldValue("inner.value"); assertThat(rejected).isNull(); + + validator.destroy(); } @Test - public void testListValidation() { + void listValidation() { + @SuppressWarnings("resource") LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); validator.afterPropertiesSet(); @@ -282,6 +313,34 @@ public void testListValidation() { assertThat(fieldError).isNotNull(); assertThat(fieldError.getRejectedValue()).isEqualTo("X"); assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); + + validator.destroy(); + } + + @Test + void withConstraintValidatorFactory() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConstraintValidatorFactory(cvf); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); + } + + @Test + void withCustomInitializer() { + ConstraintValidatorFactory cvf = new SpringConstraintValidatorFactory(new DefaultListableBeanFactory()); + + @SuppressWarnings("resource") + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setConfigurationInitializer(configuration -> configuration.constraintValidatorFactory(cvf)); + validator.afterPropertiesSet(); + + assertThat(validator.getConstraintValidatorFactory()).isSameAs(cvf); + validator.destroy(); } @@ -380,8 +439,8 @@ public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { } boolean valid = (value.name == null || !value.address.street.contains(value.name)); if (!valid && "Phil".equals(value.name)) { - context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); } return valid; } @@ -417,6 +476,7 @@ public static class InnerBean { public String getValue() { return value; } + public void setValue(String value) { this.value = value; } @@ -425,8 +485,8 @@ public void setValue(String value) { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - @Constraint(validatedBy=InnerValidator.class) - public static @interface InnerValid { + @Constraint(validatedBy = InnerValidator.class) + public @interface InnerValid { String message() default "NOT VALID"; @@ -446,7 +506,8 @@ public void initialize(InnerValid constraintAnnotation) { public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); if (bean.getValue() == null) { - context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + context.buildConstraintViolationWithTemplate("NULL") + .addPropertyNode("value").addConstraintViolation(); return false; } return true; @@ -494,7 +555,8 @@ public boolean isValid(List list, ConstraintValidatorContext context) { boolean valid = true; for (int i = 0; i < list.size(); i++) { if ("X".equals(list.get(i))) { - context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addBeanNode().inIterable().atIndex(i).addConstraintViolation(); valid = false; } } diff --git a/spring-context/src/main/java/org/springframework/validation/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/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/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/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 05809bc5ad7b..c7c5468b5896 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -65,6 +65,17 @@ public abstract class MimeTypeUtils { */ public static final String ALL_VALUE = "*/*"; + /** + * Public constant mime type for {@code application/graphql+json}. + * @see GraphQL over HTTP spec + * */ + public static final MimeType APPLICATION_GRAPHQL; + + /** + * A String equivalent of {@link MimeTypeUtils#APPLICATION_GRAPHQL}. + */ + public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; + /** * Public constant mime type for {@code application/json}. * */ @@ -165,6 +176,7 @@ public abstract class MimeTypeUtils { static { // Not using "parseMimeType" to avoid static init cost ALL = new MimeType("*", "*"); + APPLICATION_GRAPHQL = new MimeType("application", "graphql+json"); APPLICATION_JSON = new MimeType("application", "json"); APPLICATION_OCTET_STREAM = new MimeType("application", "octet-stream"); APPLICATION_XML = new MimeType("application", "xml"); diff --git a/spring-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-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index 7e09fcbe8919..274bfdfa0cbf 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,9 +98,18 @@ protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLEx if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { - String name = underscoreName(this.constructorParameterNames[i]); + String name = this.constructorParameterNames[i]; + int index; + try { + // Try direct name match first + index = rs.findColumn(lowerCaseName(name)); + } + catch (SQLException ex) { + // Try underscored name match instead + index = rs.findColumn(underscoreName(name)); + } TypeDescriptor td = this.constructorParameterTypes[i]; - Object value = getColumnValue(rs, rs.findColumn(name), td.getType()); + Object value = getColumnValue(rs, index, td.getType()); args[i] = tc.convertIfNecessary(value, td.getType(), td); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 1c0a86ffed97..cf9d5817b0c0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -20,6 +20,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Timestamp; @@ -63,7 +64,7 @@ protected void verifyPerson(Person person) { protected void verifyPerson(ConcretePerson person) { assertThat(person.getName()).isEqualTo("Bubba"); assertThat(person.getAge()).isEqualTo(22L); - assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBirthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); verifyPersonViaBeanWrapper(person); } @@ -94,7 +95,14 @@ private void verifyPersonViaBeanWrapper(Object person) { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(person); assertThat(bw.getPropertyValue("name")).isEqualTo("Bubba"); assertThat(bw.getPropertyValue("age")).isEqualTo(22L); - assertThat((Date) bw.getPropertyValue("birth_date")).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + Date birthDate; + if (bw.isReadableProperty("birth_date")) { + birthDate = (Date) bw.getPropertyValue("birth_date"); + } + else { + birthDate = (Date) bw.getPropertyValue("birthDate"); + } + assertThat(birthDate).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(bw.getPropertyValue("balance")).isEqualTo(new BigDecimal("1234.56")); } @@ -107,7 +115,7 @@ protected void verifyPerson(EmailPerson person) { } - protected enum MockType {ONE, TWO, THREE} + protected enum MockType {ONE, TWO, THREE, FOUR} protected static class Mock { @@ -152,13 +160,19 @@ public Mock(MockType type) throws Exception { given(resultSetMetaData.getColumnLabel(1)).willReturn( type == MockType.THREE ? "Last Name" : "name"); given(resultSetMetaData.getColumnLabel(2)).willReturn("age"); - given(resultSetMetaData.getColumnLabel(3)).willReturn("birth_date"); + given(resultSetMetaData.getColumnLabel(3)).willReturn(type == MockType.FOUR ? "birthdate" :"birth_date"); given(resultSetMetaData.getColumnLabel(4)).willReturn("balance"); given(resultSetMetaData.getColumnLabel(5)).willReturn("e_mail"); given(resultSet.findColumn("name")).willReturn(1); given(resultSet.findColumn("age")).willReturn(2); - given(resultSet.findColumn("birth_date")).willReturn(3); + if (type == MockType.FOUR) { + given(resultSet.findColumn("birthdate")).willReturn(3); + } + else { + given(resultSet.findColumn("birthdate")).willThrow(new SQLException()); + given(resultSet.findColumn("birth_date")).willReturn(3); + } given(resultSet.findColumn("balance")).willReturn(4); given(resultSet.findColumn("e_mail")).willReturn(5); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 99e9eb416274..5ef1f57f8916 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -140,6 +140,17 @@ void queryWithSpaceInColumnNameAndLocalDate() throws Exception { mock.verifyClosed(); } + @Test + void queryWithDirectNameMatchOnBirthDate() throws Exception { + Mock mock = new Mock(MockType.FOUR); + List result = mock.getJdbcTemplate().query( + "select name, age, birthdate, balance from people", + new BeanPropertyRowMapper<>(ConcretePerson.class)); + assertThat(result).hasSize(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + @Test void queryWithUnderscoreInColumnNameAndPersonWithMultipleAdjacentUppercaseLettersInPropertyName() throws Exception { Mock mock = new Mock(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index 48b0f7f03134..c612e5bcae63 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public void testStaticQueryWithDataClassAndGenerics() throws Exception { ConstructorPersonWithGenerics person = result.get(0); assertThat(person.name()).isEqualTo("Bubba"); assertThat(person.age()).isEqualTo(22L); - assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56"))); mock.verifyClosed(); @@ -65,15 +65,15 @@ public void testStaticQueryWithDataClassAndGenerics() throws Exception { @Test public void testStaticQueryWithDataClassAndSetters() throws Exception { - Mock mock = new Mock(); + Mock mock = new Mock(MockType.FOUR); List result = mock.getJdbcTemplate().query( - "select name, age, birth_date, balance from people", + "select name, age, birthdate, balance from people", new DataClassRowMapper<>(ConstructorPersonWithSetters.class)); assertThat(result.size()).isEqualTo(1); ConstructorPersonWithSetters person = result.get(0); assertThat(person.name()).isEqualTo("BUBBA"); assertThat(person.age()).isEqualTo(22L); - assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); mock.verifyClosed(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java index f2698d3073ac..b084644c6896 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ public abstract class AbstractPerson { private long age; - private Date birth_date; + private Date birthDate; public String getName() { @@ -46,12 +46,12 @@ public void setAge(long age) { this.age = age; } - public Date getBirth_date() { - return birth_date; + public Date getBirthDate() { + return birthDate; } - public void setBirth_date(Date birth_date) { - this.birth_date = birth_date; + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java index 3ae8e271c810..289197b56392 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithGenerics.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public class ConstructorPersonWithGenerics { private final long age; - private final Date birth_date; + private final Date birthDate; private final List balance; @@ -37,7 +37,7 @@ public class ConstructorPersonWithGenerics { public ConstructorPersonWithGenerics(String name, long age, Date birth_date, List balance) { this.name = name; this.age = age; - this.birth_date = birth_date; + this.birthDate = birth_date; this.balance = balance; } @@ -50,8 +50,8 @@ public long age() { return this.age; } - public Date birth_date() { - return this.birth_date; + public Date birthDate() { + return this.birthDate; } public List balance() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java index ef1feb9a324d..0776b5cc48ab 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPersonWithSetters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,15 +28,15 @@ public class ConstructorPersonWithSetters { private long age; - private Date birth_date; + private Date birthDate; private BigDecimal balance; - public ConstructorPersonWithSetters(String name, long age, Date birth_date, BigDecimal balance) { + public ConstructorPersonWithSetters(String name, long age, Date birthDate, BigDecimal balance) { this.name = name.toUpperCase(); this.age = age; - this.birth_date = birth_date; + this.birthDate = birthDate; this.balance = balance; } @@ -49,8 +49,8 @@ public void setAge(long age) { this.age = age; } - public void setBirth_date(Date birth_date) { - this.birth_date = birth_date; + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; } public void setBalance(BigDecimal balance) { @@ -65,8 +65,8 @@ public long age() { return this.age; } - public Date birth_date() { - return this.birth_date; + public Date birthDate() { + return this.birthDate; } public BigDecimal balance() { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java index 8dc8875e15c8..2fc59db1b2e6 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,8 +60,8 @@ public BigDecimal getBalance() { return balance; } - public void setBalance(BigDecimal balanace) { - this.balance = balanace; + public void setBalance(BigDecimal balance) { + this.balance = balance; } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index 4ace844929fb..7a834c5d2f07 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -420,21 +420,21 @@ public HandlerMethodParameter clone() { private class ReturnValueMethodParameter extends HandlerMethodParameter { @Nullable - private final Object returnValue; + private final Class returnValueType; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); - this.returnValue = returnValue; + this.returnValueType = (returnValue != null ? returnValue.getClass() : null); } protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { super(original); - this.returnValue = original.returnValue; + this.returnValueType = original.returnValueType; } @Override public Class getParameterType() { - return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); + return (this.returnValueType != null ? this.returnValueType : super.getParameterType()); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 729555add951..2a784e0386b1 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -95,6 +95,17 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_FORM_URLENCODED_VALUE = "application/x-www-form-urlencoded"; + /** + * Public constant media type for {@code application/graphql+json}. + * @see GraphQL over HTTP spec + */ + public static final MediaType APPLICATION_GRAPHQL; + + /** + * A String equivalent of {@link MediaType#APPLICATION_GRAPHQL}. + */ + public static final String APPLICATION_GRAPHQL_VALUE = "application/graphql+json"; + /** * Public constant media type for {@code application/json}. */ @@ -396,6 +407,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_ATOM_XML = new MediaType("application", "atom+xml"); APPLICATION_CBOR = new MediaType("application", "cbor"); APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded"); + APPLICATION_GRAPHQL = new MediaType("application", "graphql+json"); APPLICATION_JSON = new MediaType("application", "json"); APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8); APPLICATION_NDJSON = new MediaType("application", "x-ndjson"); 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/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 16bf30ee01fc..864e21843984 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,15 @@ * Special {@link org.springframework.validation.DataBinder} to perform data binding * from servlet request parameters to JavaBeans, including support for multipart files. * + *

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

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

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

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

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

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

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

Typical arguments are {@link org.springframework.web.bind.WebDataBinder} * in combination with {@link org.springframework.web.context.request.WebRequest} @@ -39,6 +48,7 @@ * * @author Juergen Hoeller * @since 2.5 + * @see ControllerAdvice * @see org.springframework.web.bind.WebDataBinder * @see org.springframework.web.context.request.WebRequest */ diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java index 717a6d0106ef..3316065a0760 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,18 +31,27 @@ * for controller classes with {@link RequestMapping @RequestMapping} * methods. * - *

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

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

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

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

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

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

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

By default this is set to {@code true} in which case data binding applies. diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index ed7855e79097..b4957970a5d2 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,15 @@ * Specialized {@link org.springframework.validation.DataBinder} to perform data * binding from URL query parameters or form data in the request data to Java objects. * + *

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

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

See the DataBinder/WebDataBinder superclasses for customization options, * which include specifying allowed/required fields, and registering custom * property editors. diff --git a/spring-web/src/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/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/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/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt index 56e25eb9fc51..18ab6e1dda78 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/CoroutinesIntegrationTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.jupiter.api.Disabled import org.springframework.context.ApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.ComponentScan @@ -40,7 +39,6 @@ import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpSe import reactor.core.publisher.Flux import java.time.Duration -@Disabled class CoroutinesIntegrationTests : AbstractRequestMappingIntegrationTests() { override fun initApplicationContext(): ApplicationContext { 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/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/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]#<>#