Merge branch '7.0.x'

# Conflicts:
#	spring-context/src/main/java/org/springframework/validation/DataBinder.java
This commit is contained in:
Juergen Hoeller
2026-06-22 21:55:43 +02:00
15 changed files with 198 additions and 102 deletions
@@ -49,7 +49,7 @@ import java.beans.PropertyDescriptor;
public interface BeanWrapper extends ConfigurablePropertyAccessor {
/**
* Specify a limit for array and collection auto-growing.
* Specify a limit for array and collection/set/list auto-growing.
* <p>Default is unlimited on a plain BeanWrapper.
* @since 4.1
*/
@@ -295,8 +295,13 @@ class TypeConverterDelegate {
ClassLoader cl = this.targetObject.getClass().getClassLoader();
try {
Class<?> enumValueType = ClassUtils.forName(enumType, cl);
Field enumField = enumValueType.getField(fieldName);
convertedValue = enumField.get(null);
if (enumValueType.isEnum()) {
Field enumField = enumValueType.getField(fieldName);
convertedValue = enumField.get(null);
}
else if (logger.isTraceEnabled()) {
logger.trace("Specified enum class [" + enumType + "] is not a Java enum");
}
}
catch (ClassNotFoundException ex) {
if (logger.isTraceEnabled()) {
@@ -313,8 +318,7 @@ class TypeConverterDelegate {
if (convertedValue == currentConvertedValue) {
// Try field lookup as fallback: for Java enum or custom enum
// with values defined as static fields. Resulting value still needs
// to be checked, hence we don't return it right away.
// with values defined as static fields.
try {
Field enumField = requiredType.getField(trimmedValue);
ReflectionUtils.makeAccessible(enumField);
@@ -17,15 +17,19 @@
package org.springframework.context.support;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for message source implementations, providing support infrastructure
@@ -41,6 +45,9 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class MessageSourceSupport {
static final Set<Locale> JVM_LOCALES = Arrays.stream(Locale.getAvailableLocales()).
filter(l -> StringUtils.hasLength(l.getLanguage())).collect(Collectors.toSet());
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
/** Logger available to subclasses. */
@@ -116,22 +123,17 @@ public abstract class MessageSourceSupport {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
MessageFormat messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> {
try {
return createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// Invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved...
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// Silently proceed with raw message if format not enforced...
return INVALID_MESSAGE_FORMAT;
}
});
MessageFormat messageFormat;
if (locale != null && JVM_LOCALES.contains(locale)) {
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> resolveMessageFormat(msg, key));
}
else {
messageFormat = resolveMessageFormat(msg, locale);
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
@@ -140,6 +142,29 @@ public abstract class MessageSourceSupport {
}
}
/**
* Resolve a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
* @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance, or otherwise
* {@link #INVALID_MESSAGE_FORMAT} if not resolvable
* @see #createMessageFormat
*/
private MessageFormat resolveMessageFormat(String msg, @Nullable Locale locale) {
try {
return createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// Invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved...
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// Silently proceed with raw message if format not enforced...
return INVALID_MESSAGE_FORMAT;
}
}
/**
* Create a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
@@ -40,6 +40,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
@@ -114,15 +115,19 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
private ResourceLoader resourceLoader = new DefaultResourceLoader();
// Cache to hold filename lists per Locale
// Cache to hold filename lists per Locale.
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already loaded properties per filename.
private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already merged properties per Locale.
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
// Cache to hold merged properties per non-JVM Locale.
private final ConcurrentLruCache<Locale, PropertiesHolder> customLocaleProperties =
new ConcurrentLruCache<>(64, locale -> mergeProperties(collectPropertiesToMerge(locale)));
/**
* Set the list of supported file extensions.
@@ -201,20 +206,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
return propHolder.getProperty(code);
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
@@ -229,20 +230,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
return propHolder.getMessageFormat(code, locale);
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
@@ -265,12 +262,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (mergedHolder != null) {
return mergedHolder;
}
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
if (JVM_LOCALES.contains(locale)) {
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
else {
return this.customLocaleProperties.get(locale);
}
return mergedHolder;
}
/**
@@ -289,7 +292,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
List<String> filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
PropertiesHolder propHolder = getProperties(filename, locale);
if (propHolder.getProperties() != null) {
holders.add(propHolder);
}
@@ -338,11 +341,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filenames for given Locale
// Filenames for given Locale.
List<String> filenames = new ArrayList<>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
// Filenames for default Locale, if any
// Filenames for default Locale, if any.
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && !defaultLocale.equals(locale)) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
@@ -354,24 +357,27 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filename for default bundle file
// Filename for default bundle file.
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
if (JVM_LOCALES.contains(locale)) {
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
}
localeMap.put(locale, filenames);
return filenames;
}
/**
* Calculate the filenames for the given bundle basename and Locale,
* appending language code, country code, and variant code.
* <p>For example, basename "messages", Locale "de_AT_oo" &rarr; "messages_de_AT_OO",
* <p>For example, basename "messages", Locale "de_AT_OO" &rarr; "messages_de_AT_OO",
* "messages_de_AT", "messages_de".
* <p>Follows the rules defined by {@link java.util.Locale#toString()}.
* @param basename the basename of the bundle
@@ -406,6 +412,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
* @param filename the bundle filename (basename + Locale)
* @param locale the requested locale (for cache filtering)
* @return the current PropertiesHolder for the bundle
* @see #getProperties(String)
*/
private PropertiesHolder getProperties(String filename, Locale locale) {
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() == null && !JVM_LOCALES.contains(locale)) {
this.cachedProperties.remove(filename);
}
return propHolder;
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
@@ -188,36 +188,35 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
* found for the given basename and Locale
*/
protected @Nullable ResourceBundle getResourceBundle(String basename, Locale locale) {
if (getCacheMillis() >= 0) {
if (getCacheMillis() >= 0 || !JVM_LOCALES.contains(locale)) {
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
// do its native caching, at the expense of more extensive lookup steps.
return doGetBundle(basename, locale);
}
else {
// Cache forever: prefer locale cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
// Cache forever: prefer local cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
}
@@ -311,6 +310,11 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
protected @Nullable MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
throws MissingResourceException {
if (!JVM_LOCALES.contains(locale)) {
String msg = getStringOrNull(bundle, code);
return (msg != null ? createMessageFormat(msg, locale) : null);
}
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
Map<Locale, MessageFormat> localeMap = null;
if (codeMap != null) {
@@ -270,12 +270,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Specify the limit for array and collection auto-growing.
* Specify the limit for array and collection/set/list auto-growing.
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
* Raise this limit if your auto-growing needs are unusually high.
* <p>Used for setter injection - and as of 7.1 also for field injection -
* via {@link #bind(PropertyValues)}; not applicable to constructor binding
* via {@link #construct}.
* via {@link #bind(PropertyValues)}; not applicable to map properties and
* not to constructor binding via {@link #construct} either.
* @see #initBeanPropertyAccess()
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
*/
@@ -326,6 +326,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
/**
* Initialize direct field access for this DataBinder,
* as alternative to the default bean property access.
* <p><b>NOTE: This is an advanced option for trusted scenarios.</b>
* Do not use direct field access for data binding from untrusted sources.
* @see #initBeanPropertyAccess()
* @see #createDirectFieldBindingResult()
*/
@@ -100,20 +100,20 @@ public class LocalizedResourceHelper {
String variant = locale.getVariant();
// Check for file with language, country and variant localization.
if (variant.length() > 0) {
if (!variant.isEmpty()) {
String location =
name + this.separator + lang + this.separator + country + this.separator + variant + extension;
resource = this.resourceLoader.getResource(location);
}
// Check for file with language and country localization.
if ((resource == null || !resource.exists()) && country.length() > 0) {
if ((resource == null || !resource.exists()) && !country.isEmpty()) {
String location = name + this.separator + lang + this.separator + country + extension;
resource = this.resourceLoader.getResource(location);
}
// Check for document with language localization.
if ((resource == null || !resource.exists()) && lang.length() > 0) {
if ((resource == null || !resource.exists()) && !lang.isEmpty()) {
String location = name + this.separator + lang + extension;
resource = this.resourceLoader.getResource(location);
}
@@ -18,6 +18,7 @@ package org.springframework.core.serializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import org.jspecify.annotations.Nullable;
@@ -38,6 +39,8 @@ public class DefaultDeserializer implements Deserializer<Object> {
private final @Nullable ClassLoader classLoader;
private final @Nullable ObjectInputFilter objectInputFilter;
/**
* Create a {@code DefaultDeserializer} with default {@link ObjectInputStream}
@@ -45,6 +48,7 @@ public class DefaultDeserializer implements Deserializer<Object> {
*/
public DefaultDeserializer() {
this.classLoader = null;
this.objectInputFilter = null;
}
/**
@@ -56,6 +60,21 @@ public class DefaultDeserializer implements Deserializer<Object> {
*/
public DefaultDeserializer(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
this.objectInputFilter = null;
}
/**
* Create a {@code DefaultDeserializer} for using an {@link ObjectInputStream}
* with the given {@code ClassLoader}.
* @param classLoader the ClassLoader to use
* @param objectInputFilter a custom ObjectInputFilter to apply
* @since 7.0.9
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
* @see ObjectInputStream#setObjectInputFilter
*/
public DefaultDeserializer(@Nullable ClassLoader classLoader, @Nullable ObjectInputFilter objectInputFilter) {
this.classLoader = classLoader;
this.objectInputFilter = objectInputFilter;
}
@@ -65,10 +84,20 @@ public class DefaultDeserializer implements Deserializer<Object> {
* @since 6.2.19
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
*/
public @Nullable ClassLoader getClassLoader() {
public final @Nullable ClassLoader getClassLoader() {
return this.classLoader;
}
/**
* Return the {@link ObjectInputFilter} to apply to the {@link ObjectInputStream},
* if any.
* @since 7.0.9
* @see ObjectInputStream#setObjectInputFilter
*/
public final @Nullable ObjectInputFilter getObjectInputFilter() {
return this.objectInputFilter;
}
/**
* Read from the supplied {@code InputStream} and deserialize the contents
@@ -78,6 +107,9 @@ public class DefaultDeserializer implements Deserializer<Object> {
@Override
public Object deserialize(InputStream inputStream) throws IOException {
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
if (this.objectInputFilter != null) {
objectInputStream.setObjectInputFilter(this.objectInputFilter);
}
try {
return objectInputStream.readObject();
}
@@ -118,7 +118,7 @@ public abstract class FileSystemUtils {
/**
* Recursively copy the contents of the {@code src} file/directory
* to the {@code dest} file/directory.
* to the {@code dest} file/directory, including symbolic links.
* @param src the source directory
* @param dest the destination directory
* @throws IOException in the case of I/O errors
@@ -956,6 +956,7 @@ public abstract class StringUtils {
String country = tokens[1];
validateLocalePart(country);
String variant = Arrays.stream(tokens).skip(2).collect(Collectors.joining(delimiter));
validateLocalePart(variant);
return new Locale(language, country, variant);
}
@@ -58,7 +58,7 @@ public class SqliteMaxValueIncrementer extends AbstractColumnMaxValueIncrementer
@Override
protected long getNextKey() {
protected synchronized long getNextKey() {
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
@@ -30,6 +30,9 @@ import org.springframework.util.Assert;
* if the destination resolving process is expensive (for example, the destination has to be
* resolved through an external system) and the resolution results are stable anyway.
*
* <p>Note: This cache is not designed for setups with varying dynamic destination names.
* Prefer direct destination resolution in such scenarios.
*
* @author Agim Emruli
* @author Juergen Hoeller
* @since 4.1
@@ -83,6 +83,8 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer
* Set whether to use direct field access instead of bean property access.
* <p>Default is {@code false}, using bean property access.
* Switch this to {@code true} in order to enforce direct field access.
* <p><b>NOTE: This is an advanced option for trusted scenarios.</b>
* Do not use direct field access for data binding from untrusted sources.
* @see org.springframework.validation.DataBinder#initDirectFieldAccess()
* @see org.springframework.validation.DataBinder#initBeanPropertyAccess()
*/
@@ -124,7 +124,7 @@ class HtmlCharacterEntityDecoder {
int value = (!isHexNumberedReference ?
Integer.parseInt(getReferenceSubstring(2)) :
Integer.parseInt(getReferenceSubstring(3), 16));
if (value > Character.MAX_CODE_POINT) {
if (value < 0 || value > Character.MAX_CODE_POINT) {
return false;
}
this.decodedMessage.appendCodePoint(value);
@@ -51,6 +51,7 @@ public class ParameterizableViewController extends AbstractController {
setSupportedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name());
}
/**
* Set a view name for the ModelAndView to return, to be resolved by the
* DispatcherServlet via a ViewResolver. Will override any pre-existing
@@ -117,12 +118,11 @@ public class ParameterizableViewController extends AbstractController {
return this.statusCode;
}
/**
* The property can be used to indicate the request is considered fully
* handled within the controller and that no view should be used for rendering.
* Useful in combination with {@link #setStatusCode}.
* <p>By default this is set to {@code false}.
* <p>By default, this is set to {@code false}.
* @since 4.1
*/
public void setStatusOnly(boolean statusOnly) {
@@ -187,10 +187,11 @@ public class ParameterizableViewController extends AbstractController {
sb.append("status=").append(this.statusCode);
}
if (this.view != null) {
sb.append(sb.length() != 0 ? ", " : "");
sb.append(!sb.isEmpty() ? ", " : "");
String viewName = getViewName();
sb.append("view=").append(viewName != null ? "\"" + viewName + "\"" : this.view);
}
return sb.toString();
}
}