Compare commits

...

24 Commits

Author SHA1 Message Date
Brian Clozel 99a366baf6 Next development version (v6.1.22-SNAPSHOT) 2025-06-12 10:56:22 +02:00
Brian Clozel 498ccda8fc Upgrade to Gradle 8.14.2 2025-06-12 10:00:55 +02:00
Brian Clozel fd68ea6fcb Encode non-printable character in Content-Disposition parameter
Prior to this commit, the "filename" parameter value for the
"Content-Disposition" header would contain non-printable characters,
causing parsing issues for HTTP clients.
This commit ensures that all non-printable characters are encoded.

Fixes gh-35035
2025-06-12 08:39:41 +02:00
Juergen Hoeller 28caa39020 Upgrade to Reactor 2023.0.19
Includes SLF4J 2.0.17, Groovy 4.0.27, FreeMarker 2.3.34

Closes gh-35022
2025-06-11 12:54:02 +02:00
Sam Brannen 8ecc553696 Polish contribution
See gh-35013
See gh-35018

(cherry picked from commit 4d2cc4ae97)
2025-06-10 12:39:48 +02:00
Mohammad Saeed Nouri cd44efaf68 Allow update of existing WebSession after max sessions limit is reached
Previously, when saving a WebSession, the system did not check whether
the session ID already existed. As a result, even if the session being
saved was an update to an existing one, it was incorrectly treated as a
new session, and a "maximum sessions exceeded" error was triggered.

This fix ensures that if a WebSession with the same ID already exists,
it will be updated rather than counted as a new session, thereby
preventing unnecessary session limit violations.

See gh-35013
Closes gh-35018

Signed-off-by: Mohammad Saeed Nouri <msnsaeed71@gmail.com>
(cherry picked from commit c04902fefb)
2025-06-10 12:38:58 +02:00
Sam Brannen 59d2895c82 Fix InMemoryWebSessionStoreTests.startsSessionImplicitly() test
(cherry picked from commit 3c265e1044)
2025-06-10 12:37:45 +02:00
Sam Brannen a876bb41af Polish WebSession support and tests
(cherry picked from commit 222702f750)
2025-06-10 12:37:19 +02:00
Juergen Hoeller 3b6becac01 Check for package-visible constructor in case of ClassLoader mismatch
Closes gh-34950

(cherry picked from commit 15d1455acb)
2025-05-27 10:30:07 +02:00
Sam Brannen 59ffbd7a59 Test conversion support in PropertySourcesPlaceholderConfigurer
This commit introduces a "regression test" which demonstrates that
PropertySourcesPlaceholderConfigurer uses the ConversionService from
the Environment.

See gh-34936
2025-05-26 18:09:51 +02:00
Sam Brannen c0a9da65f4 Support only Object property values in MockEnvironment test fixture
The setProperty() and withProperty() methods in MockEnvironment were
originally introduced with (String, String) signatures; however, they
should have always had (String, Object) signatures in order to comply
with the MockPropertySource and PropertySource APIs.

To address that, this commit changes the signatures of these methods so
that they only accept Object values for properties.

NOTE: this commit only affects the internal MockEnvironment used as a
test fixture. This commit does not affect the official, public
MockEnvironment implementation in spring-test.

See gh-34947
See gh-34948

(cherry picked from commit d78264756e)
2025-05-26 17:58:15 +02:00
Sam Brannen 30bee4db0f Polish Javadoc for MockPropertySource
(cherry picked from commit 6a6abac003)
2025-05-26 17:54:52 +02:00
Brian Clozel 9a89654a64 Next development version (v6.1.21-SNAPSHOT) 2025-05-15 10:32:10 +02:00
rstoyanchev edfcc6ffb1 Make use of PatternMatchUtils ignoreCase option
Closes gh-34801
2025-05-14 15:49:06 +01:00
rstoyanchev f93132b11e Add missing @since tags in PatternMatchUtils
See: gh-34801
2025-05-14 15:17:56 +01:00
Juergen Hoeller 6ab4c84bd5 Upgrade to Reactor 2023.0.18
Closes gh-34899
2025-05-14 15:25:15 +02:00
Juergen Hoeller d5fca0d2c5 Upgrade to Jetty 12.0.21, Netty 4.1.121, Apache HttpClient 5.4.4, Checkstyle 10.23.1 2025-05-13 16:57:19 +02:00
Juergen Hoeller cbb94193fe Clarify CompositePropertySource behavior for EnumerablePropertySource contract
Closes gh-34886

(cherry picked from commit 6a9444473f)
2025-05-13 16:57:06 +02:00
Brian Clozel 5b5e2b6876 Fix HttpClient 5.3.x request config compatibility
As of gh-33806, the HttpComponents client request factory is forward
compatible with the 5.4+ versions of that library for configuring HTTP
request configuration.

This change would not tkae into account configuration set at the Spring
level because it would consider the default `RequestConfig` instance as
a custom one. This commit ensures that Spring sets its own configuration
if no custom configuration was set for all supported HttpComponents
generations.

Fixes gh-34854
2025-05-05 14:50:46 +02:00
Juergen Hoeller a5b0399a1d Polishing
(cherry picked from commit d0b186a1c7)
2025-04-28 16:20:29 +02:00
Juergen Hoeller 71f2725638 Try loadClass on LinkageError in case of same ClassLoader as well
Closes gh-34824

(cherry picked from commit 4172581f1b)
2025-04-28 16:20:24 +02:00
Sam Brannen daee9f1242 Reinstate the @⁠Inject Technology Compatibility Kit (TCK)
In commit 05ebca8677, the `public` modifier was removed from the
SpringAtInjectTckTests class, which prevents it from being run as a
JUnit 3 test class.

To address that, this commit adds the missing `public` modifier as well
as a a code comment to help prevent this from happening again.

In addition, this commit updates spring-context.gradle to ensure that
the JUnit Vintage test engine is always applied. However, that Gradle
configuration is unfortunately ignored due to how our TestConventions
class has been implemented. Thus, that issue will have to be addressed
separately.

Closes gh-34800

(cherry picked from commit e384389790)
2025-04-25 12:37:58 +02:00
rstoyanchev 89ae20b9ff Add ignoreCase variants to PatternMatchUtils
See gh-34801
2025-04-23 12:13:49 +01:00
Brian Clozel a5a7d63dab Next development version (v6.1.20-SNAPSHOT) 2025-04-17 09:05:28 +02:00
27 changed files with 350 additions and 120 deletions
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.22.0");
checkstyle.setToolVersion("10.23.1");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+8 -8
View File
@@ -9,15 +9,15 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
api(platform("io.micrometer:micrometer-bom:1.12.12"))
api(platform("io.netty:netty-bom:4.1.119.Final"))
api(platform("io.netty:netty-bom:4.1.121.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2023.0.16"))
api(platform("io.projectreactor:reactor-bom:2023.0.19"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.18"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.10.5"))
@@ -101,7 +101,7 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.4.3")
api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
@@ -121,7 +121,7 @@ dependencies {
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.10.8")
api("org.ehcache:jcache:1.0.1")
api("org.freemarker:freemarker:2.3.33")
api("org.freemarker:freemarker:2.3.34")
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
api("org.glassfish:jakarta.el:4.0.2")
api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3")
@@ -140,7 +140,7 @@ dependencies {
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.16")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.9.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.1.19-SNAPSHOT
version=6.1.22-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+7
View File
@@ -58,3 +58,10 @@ dependencies {
testRuntimeOnly("org.javamoney:moneta")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK
}
test {
description = "Runs JUnit Jupiter tests and the @Inject TCK via JUnit Vintage."
useJUnitPlatform {
includeEngines "junit-jupiter", "junit-vintage"
}
}
@@ -16,6 +16,7 @@
package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -139,14 +140,20 @@ class ConfigurationClassEnhancer {
}
/**
* Checks whether the given config class relies on package visibility,
* either for the class itself or for any of its {@code @Bean} methods.
* Checks whether the given config class relies on package visibility, either for
* the class and any of its constructors or for any of its {@code @Bean} methods.
*/
private boolean reliesOnPackageVisibility(Class<?> configSuperClass) {
int mod = configSuperClass.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
for (Constructor<?> ctor : configSuperClass.getDeclaredConstructors()) {
mod = ctor.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
}
for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) {
if (BeanAnnotationHelper.isBeanAnnotated(method)) {
mod = method.getModifiers();
@@ -27,7 +27,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -543,15 +542,13 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* <p>Mark fields as disallowed, for example to avoid unwanted
* modifications by malicious users when binding HTTP request parameters.
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>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.
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts),
* as well as direct equality.
* <p>The default implementation of this method stores disallowed field
* patterns in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String)
* canonical} form, and subsequently pattern matching in {@link #isAllowed}
* is case-insensitive. Subclasses that override this method must therefore
* take this transformation into account.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
@@ -569,8 +566,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
fieldPatterns[i] = field.toLowerCase(Locale.ROOT);
fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
}
this.disallowedFields = fieldPatterns;
}
@@ -1140,9 +1136,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Determine if the given field is allowed for binding.
* <p>Invoked for each passed-in property value.
* <p>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.
* {@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.
* <p>Matching against allowed field patterns is case-sensitive; whereas,
* matching against disallowed field patterns is case-insensitive.
* <p>A field matching a disallowed pattern will not be accepted even if it
@@ -1158,8 +1154,13 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
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.toLowerCase(Locale.ROOT))));
if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) {
return false;
}
if (!ObjectUtils.isEmpty(disallowed)) {
return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field);
}
return true;
}
/**
@@ -104,6 +104,31 @@ class ConfigurationClassEnhancerTests {
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicConstructor() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicMethod() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
@@ -160,6 +185,19 @@ class ConfigurationClassEnhancerTests {
}
@Configuration
public static class MyConfigWithNonPublicConstructor {
MyConfigWithNonPublicConstructor() {
}
@Bean
public String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithNonPublicMethod {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,8 @@ import org.springframework.context.support.GenericApplicationContext;
* @author Juergen Hoeller
* @since 3.0
*/
class SpringAtInjectTckTests {
// WARNING: This class MUST be public, since it is based on JUnit 3.
public class SpringAtInjectTckTests {
@SuppressWarnings("unchecked")
public static Test suite() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author 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,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
@@ -72,6 +73,33 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(ppc.getAppliedPropertySources()).isNotNull();
}
@Test // gh-34936
void replacementFromEnvironmentPropertiesWithConversion() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${my.name}")
.getBeanDefinition());
record Point(int x, int y) {
}
Converter<Point, String> pointToStringConverter =
point -> "(%d,%d)".formatted(point.x, point.y);
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(Point.class, String.class, pointToStringConverter);
MockEnvironment env = new MockEnvironment();
env.setConversionService(conversionService);
env.setProperty("my.name", new Point(4,5));
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
ppc.setEnvironment(env);
ppc.postProcessBeanFactory(bf);
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("(4,5)");
}
@Test
void localPropertiesViaResource() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author 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 @@ import org.springframework.core.testfixture.env.MockPropertySource;
* @author Chris Beams
* @author Sam Brannen
* @since 3.2
* @see org.springframework.core.testfixture.env.MockPropertySource
* @see MockPropertySource
*/
public class MockEnvironment extends AbstractEnvironment {
@@ -44,19 +44,21 @@ public class MockEnvironment extends AbstractEnvironment {
/**
* Set a property on the underlying {@link MockPropertySource} for this environment.
* @see MockPropertySource#setProperty(String, Object)
*/
public void setProperty(String key, String value) {
this.propertySource.setProperty(key, value);
public void setProperty(String name, Object value) {
this.propertySource.setProperty(name, value);
}
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* Convenient synonym for {@link #setProperty(String, Object)} that returns
* the current instance.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @see MockPropertySource#withProperty
* @see MockPropertySource#withProperty(String, Object)
*/
public MockEnvironment withProperty(String key, String value) {
setProperty(key, value);
public MockEnvironment withProperty(String name, Object value) {
setProperty(name, value);
return this;
}
@@ -463,10 +463,21 @@ public class ReflectUtils {
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalArgumentException ex) {
// in case of plain LinkageError (class already defined)
// or IllegalArgumentException (class in different package):
// fall through to traditional ClassLoader.defineClass below
t = ex;
if (ex instanceof LinkageError) {
// Could be a ClassLoader mismatch with the class pre-existing in a
// parent ClassLoader -> try loadClass before giving up completely.
try {
c = contextClass.getClassLoader().loadClass(className);
}
catch (ClassNotFoundException cnfe) {
}
}
if (c == null) {
// in case of plain LinkageError (class already defined)
// or IllegalArgumentException (class in different package):
// fall through to traditional ClassLoader.defineClass below
t = ex;
}
}
catch (Throwable ex) {
throw new CodeGenerationException(ex);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +33,10 @@ import org.springframework.util.StringUtils;
*
* <p>As of Spring 4.1.2, this class extends {@link EnumerablePropertySource} instead
* of plain {@link PropertySource}, exposing {@link #getPropertyNames()} based on the
* accumulated property names from all contained sources (as far as possible).
* accumulated property names from all contained sources - and failing with an
* {@code IllegalStateException} against any non-{@code EnumerablePropertySource}.
* <b>When used through the {@code EnumerablePropertySource} contract, all contained
* sources are expected to be of type {@code EnumerablePropertySource} as well.</b>
*
* @author Chris Beams
* @author Juergen Hoeller
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,13 +37,25 @@ public abstract class PatternMatchUtils {
* @return whether the String matches the given pattern
*/
public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) {
return simpleMatch(pattern, str, false);
}
/**
* Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case.
* @since 6.1.20
*/
public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) {
return simpleMatch(pattern, str, true);
}
private static boolean simpleMatch(@Nullable String pattern, @Nullable String str, boolean ignoreCase) {
if (pattern == null || str == null) {
return false;
}
int firstIndex = pattern.indexOf('*');
if (firstIndex == -1) {
return pattern.equals(str);
return (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str));
}
if (firstIndex == 0) {
@@ -52,25 +64,43 @@ public abstract class PatternMatchUtils {
}
int nextIndex = pattern.indexOf('*', 1);
if (nextIndex == -1) {
return str.endsWith(pattern.substring(1));
String part = pattern.substring(1);
return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part));
}
String part = pattern.substring(1, nextIndex);
if (part.isEmpty()) {
return simpleMatch(pattern.substring(nextIndex), str);
return simpleMatch(pattern.substring(nextIndex), str, ignoreCase);
}
int partIndex = str.indexOf(part);
int partIndex = indexOf(str, part, 0, ignoreCase);
while (partIndex != -1) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) {
return true;
}
partIndex = str.indexOf(part, partIndex + 1);
partIndex = indexOf(str, part, partIndex + 1, ignoreCase);
}
return false;
}
return (str.length() >= firstIndex &&
pattern.startsWith(str.substring(0, firstIndex)) &&
simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex)));
checkStartsWith(pattern, str, firstIndex, ignoreCase) &&
simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase));
}
private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) {
String part = str.substring(0, index);
return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part));
}
private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) {
if (!ignoreCase) {
return str.indexOf(otherStr, startIndex);
}
for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) {
if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) {
return i;
}
}
return -1;
}
/**
@@ -94,4 +124,19 @@ public abstract class PatternMatchUtils {
return false;
}
/**
* Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case.
* @since 6.1.20
*/
public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) {
if (patterns != null) {
for (String pattern : patterns) {
if (simpleMatch(pattern, str, true)) {
return true;
}
}
}
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -85,6 +85,7 @@ public class ExponentialBackOff implements BackOff {
*/
public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE;
private long initialInterval = DEFAULT_INITIAL_INTERVAL;
private double multiplier = DEFAULT_MULTIPLIER;
@@ -204,6 +205,7 @@ public class ExponentialBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new ExponentialBackOffExecution();
@@ -225,6 +227,7 @@ public class ExponentialBackOff implements BackOff {
.toString();
}
private class ExponentialBackOffExecution implements BackOffExecution {
private long currentInterval = -1;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2025 the original author 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,7 @@ public class FixedBackOff implements BackOff {
*/
public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE;
private long interval = DEFAULT_INTERVAL;
private long maxAttempts = UNLIMITED_ATTEMPTS;
@@ -86,6 +87,7 @@ public class FixedBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new FixedBackOffExecution();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,18 +53,22 @@ class PatternMatchUtilsTests {
assertMatches(new String[] { null, "" }, "");
assertMatches(new String[] { null, "123" }, "123");
assertMatches(new String[] { null, "*" }, "123");
testMixedCaseMatch("abC", "Abc");
}
@Test
void startsWith() {
assertMatches("get*", "getMe");
assertDoesNotMatch("get*", "setMe");
testMixedCaseMatch("geT*", "GetMe");
}
@Test
void endsWith() {
assertMatches("*Test", "getMeTest");
assertDoesNotMatch("*Test", "setMe");
testMixedCaseMatch("*TeSt", "getMeTesT");
}
@Test
@@ -74,6 +78,10 @@ class PatternMatchUtilsTests {
assertMatches("*stuff*", "stuffTest");
assertMatches("*stuff*", "getstuff");
assertMatches("*stuff*", "stuff");
testMixedCaseMatch("*stuff*", "getStuffTest");
testMixedCaseMatch("*stuff*", "StuffTest");
testMixedCaseMatch("*stuff*", "getStuff");
testMixedCaseMatch("*stuff*", "Stuff");
}
@Test
@@ -82,6 +90,8 @@ class PatternMatchUtilsTests {
assertMatches("on*Event", "onEvent");
assertDoesNotMatch("3*3", "3");
assertMatches("3*3", "33");
testMixedCaseMatch("on*Event", "OnMyEvenT");
testMixedCaseMatch("on*Event", "OnEvenT");
}
@Test
@@ -122,18 +132,27 @@ class PatternMatchUtilsTests {
private void assertMatches(String pattern, String str) {
assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue();
assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue();
}
private void assertDoesNotMatch(String pattern, String str) {
assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse();
assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isFalse();
}
private void testMixedCaseMatch(String pattern, String str) {
assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse();
assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue();
}
private void assertMatches(String[] patterns, String str) {
assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue();
assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isTrue();
}
private void assertDoesNotMatch(String[] patterns, String str) {
assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse();
assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isFalse();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -36,7 +36,7 @@ import org.springframework.core.env.PropertySource;
*
* @author Chris Beams
* @since 3.1
* @see org.springframework.mock.env.MockEnvironment
* @see MockEnvironment
*/
public class MockPropertySource extends PropertiesPropertySource {
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,6 +67,7 @@ public final class ContentDisposition {
for (int i=33; i<= 126; i++) {
PRINTABLE.set(i);
}
PRINTABLE.set(34, false); // "
PRINTABLE.set(61, false); // =
PRINTABLE.set(63, false); // ?
PRINTABLE.set(95, false); // _
@@ -216,9 +216,8 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
context = HttpClientContext.create();
}
// Request configuration not set in the context
if (!(context instanceof HttpClientContext clientContext && clientContext.getRequestConfig() != null) &&
context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
// No custom request configuration was set
if (!hasCustomRequestConfig(context)) {
RequestConfig config = null;
// Use request configuration given by the user, when available
if (httpRequest instanceof Configurable configurable) {
@@ -237,6 +236,18 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
return new HttpComponentsClientHttpRequest(client, httpRequest, context);
}
@SuppressWarnings("deprecation") // HttpClientContext.REQUEST_CONFIG
private static boolean hasCustomRequestConfig(HttpContext context) {
if (context instanceof HttpClientContext clientContext) {
// Prior to 5.4, the default config was set to RequestConfig.DEFAULT
// As of 5.4, it is set to null
RequestConfig requestConfig = clientContext.getRequestConfig();
return requestConfig != null && !requestConfig.equals(RequestConfig.DEFAULT);
}
// Prior to 5.4, the config was stored as an attribute
return context.getAttribute(HttpClientContext.REQUEST_CONFIG) != null;
}
/**
* Create a default {@link RequestConfig} to use with the given client.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -107,12 +107,12 @@ public interface ServerWebExchange {
}
/**
* Return the web session for the current request. Always guaranteed to
* return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not
* specify one or because the underlying session had expired. Use of this
* method does not automatically create a session. See {@link WebSession}
* for more details.
* Return the web session for the current request.
* <p>Always guaranteed to return either an instance matching the session id
* requested by the client, or a new session either because the client did not
* specify a session id or because the underlying session expired.
* <p>Use of this method does not automatically create a session. See
* {@link WebSession} for more details.
*/
Mono<WebSession> getSession();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -79,10 +79,10 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Configure the {@link Clock} to use to set lastAccessTime on every created
* session and to calculate if it is expired.
* <p>This may be useful to align to different timezone or to set the clock
* back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* Configure the {@link Clock} to use to set the {@code lastAccessTime} on
* every created session and to calculate if the session has expired.
* <p>This may be useful to align to different time zones or to set the clock
* back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* in order to simulate session expiration.
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
* @param clock the clock to use
@@ -94,16 +94,17 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Return the configured clock for session lastAccessTime calculations.
* Return the configured clock for session {@code lastAccessTime} calculations.
*/
public Clock getClock() {
return this.clock;
}
/**
* Return the map of sessions with an {@link Collections#unmodifiableMap
* unmodifiable} wrapper. This could be used for management purposes, to
* list active sessions, invalidate expired ones, etc.
* Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the
* map of sessions.
* <p>This could be used for management purposes, to list active sessions,
* to invalidate expired sessions, etc.
* @since 5.0.8
*/
public Map<String, WebSession> getSessions() {
@@ -157,10 +158,11 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Check for expired sessions and remove them. Typically such checks are
* kicked off lazily during calls to {@link #createWebSession() create} or
* {@link #retrieveSession retrieve}, no less than 60 seconds apart.
* This method can be called to force a check at a specific time.
* Check for expired sessions and remove them.
* <p>Typically such checks are kicked off lazily during calls to
* {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
* seconds apart.
* <p>This method can be called to force a check at a specific time.
* @since 5.0.8
*/
public void removeExpiredSessions() {
@@ -278,7 +280,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private void checkMaxSessionsLimit() {
if (sessions.size() >= maxSessions) {
expiredSessionChecker.removeExpiredSessions(clock.instant());
if (sessions.size() >= maxSessions) {
if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) {
throw new IllegalStateException("Max sessions limit reached: " + sessions.size());
}
}
@@ -32,10 +32,10 @@ import org.springframework.web.server.WebSession;
public interface WebSessionManager {
/**
* Return the {@link WebSession} for the given exchange. Always guaranteed
* to return an instance either matching to the session id requested by the
* client, or a new session either because the client did not specify one
* or because the underlying session expired.
* Return the {@link WebSession} for the given exchange.
* <p>Always guaranteed to return either an instance matching the session id
* requested by the client, or a new session either because the client did not
* specify a session id or because the underlying session expired.
* @param exchange the current exchange
* @return promise for the WebSession
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ public interface WebSessionStore {
* Return the WebSession for the given id.
* <p><strong>Note:</strong> This method should perform an expiration check,
* and if it has expired remove the session and return empty. This method
* should also update the lastAccessTime of retrieved sessions.
* should also update the {@code lastAccessTime} of retrieved sessions.
* @param sessionId the session to load
* @return the session, or an empty {@code Mono}
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -305,6 +305,13 @@ class ContentDispositionTests {
tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\");
}
@Test
void formatWithUtf8FilenameWithQuotes() {
String filename = "\"中文.txt";
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
.isEqualTo("form-data; filename=\"=?UTF-8?Q?=22=E4=B8=AD=E6=96=87.txt?=\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
}
@Test
void formatWithEncodedFilenameUsingInvalidCharset() {
assertThatIllegalArgumentException().isThrownBy(() ->
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@ package org.springframework.web.server.session;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.web.server.WebSession;
@@ -35,10 +35,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link InMemoryWebSessionStore}.
*
* @author Rob Winch
* @author Sam Brannen
*/
class InMemoryWebSessionStoreTests {
private InMemoryWebSessionStore store = new InMemoryWebSessionStore();
private final InMemoryWebSessionStore store = new InMemoryWebSessionStore();
@Test
@@ -53,13 +54,14 @@ class InMemoryWebSessionStoreTests {
void startsSessionImplicitly() {
WebSession session = this.store.createWebSession().block();
assertThat(session).isNotNull();
session.start();
// We intentionally do not invoke start().
// session.start();
session.getAttributes().put("foo", "bar");
assertThat(session.isStarted()).isTrue();
}
@Test // gh-24027, gh-26958
public void createSessionDoesNotBlock() {
void createSessionDoesNotBlock() {
this.store.createWebSession()
.doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue())
.block();
@@ -103,7 +105,7 @@ class InMemoryWebSessionStoreTests {
}
@Test // SPR-17051
public void sessionInvalidatedBeforeSave() {
void sessionInvalidatedBeforeSave() {
// Request 1 creates session
WebSession session1 = this.store.createWebSession().block();
assertThat(session1).isNotNull();
@@ -132,33 +134,69 @@ class InMemoryWebSessionStoreTests {
@Test
void expirationCheckPeriod() {
DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
Map<?,?> sessions = (Map<?, ?>) accessor.getPropertyValue("sessions");
assertThat(sessions).isNotNull();
// Create 100 sessions
IntStream.range(0, 100).forEach(i -> insertSession());
assertThat(sessions).hasSize(100);
IntStream.rangeClosed(1, 100).forEach(i -> insertSession());
assertNumSessions(100);
// Force a new clock (31 min later), don't use setter which would clean expired sessions
// Force a new clock (31 min later). Don't use setter which would clean expired sessions.
DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
accessor.setPropertyValue("clock", Clock.offset(this.store.getClock(), Duration.ofMinutes(31)));
assertThat(sessions).hasSize(100);
assertNumSessions(100);
// Create 1 more which forces a time-based check (clock moved forward)
// Create 1 more which forces a time-based check (clock moved forward).
insertSession();
assertThat(sessions).hasSize(1);
assertNumSessions(1);
}
@Test
void maxSessions() {
this.store.setMaxSessions(10);
IntStream.range(0, 10000).forEach(i -> insertSession());
assertThatIllegalStateException().isThrownBy(
this::insertSession)
.withMessage("Max sessions limit reached: 10000");
IntStream.rangeClosed(1, 10).forEach(i -> insertSession());
assertThatIllegalStateException()
.isThrownBy(this::insertSession)
.withMessage("Max sessions limit reached: 10");
}
@Test
void updateSession() {
WebSession session = insertSession();
StepVerifier.create(session.save())
.expectComplete()
.verify();
}
@Test // gh-35013
void updateSessionAfterMaxSessionLimitIsExceeded() {
this.store.setMaxSessions(10);
WebSession session = insertSession();
assertNumSessions(1);
IntStream.rangeClosed(1, 9).forEach(i -> insertSession());
assertNumSessions(10);
// Updating an existing session should succeed.
StepVerifier.create(session.save())
.expectComplete()
.verify();
assertNumSessions(10);
// Saving an additional new session should fail.
assertThatIllegalStateException()
.isThrownBy(this::insertSession)
.withMessage("Max sessions limit reached: 10");
assertNumSessions(10);
// Updating an existing session again should still succeed.
StepVerifier.create(session.save())
.expectComplete()
.verify();
assertNumSessions(10);
}
private WebSession insertSession() {
WebSession session = this.store.createWebSession().block();
assertThat(session).isNotNull();
@@ -167,4 +205,8 @@ class InMemoryWebSessionStoreTests {
return session;
}
private void assertNumSessions(int numSessions) {
assertThat(store.getSessions()).hasSize(numSessions);
}
}