From c942c04aa06df06ed2da93cc9216b9095e3d59fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 3 Aug 2023 11:35:52 +0200 Subject: [PATCH] Support resource bundle custom file extensions This commit allows to configure custom file extensions in ReloadableResourceBundleMessageSource thanks to a new setFileExtensions setter. Combined with setPropertiesPersister, it allows custom implementations supporting any kind of property file. Closes gh-18990 --- ...ReloadableResourceBundleMessageSource.java | 44 ++++++++++++++----- .../ResourceBundleMessageSourceTests.java | 30 ++++++++++++- .../context/support/messages.custom | 2 + .../context/support/messages_de.custom | 1 + 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 spring-context/src/test/resources/org/springframework/context/support/messages.custom create mode 100644 spring-context/src/test/resources/org/springframework/context/support/messages_de.custom diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 74b23f55f7a..0ce1b8cd0fc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -21,9 +21,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -34,6 +36,8 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -74,6 +78,7 @@ import org.springframework.util.StringUtils; * this message source! * * @author Juergen Hoeller + * @author Sebastien Deleuze * @see #setCacheSeconds * @see #setBasenames * @see #setDefaultEncoding @@ -87,10 +92,10 @@ import org.springframework.util.StringUtils; public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements ResourceLoaderAware { - private static final String PROPERTIES_SUFFIX = ".properties"; + private static final String XML_EXTENSION = ".xml"; - private static final String XML_SUFFIX = ".xml"; + private List fileExtensions = List.of(".properties", XML_EXTENSION); @Nullable private Properties fileEncodings; @@ -111,6 +116,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased private final ConcurrentMap cachedMergedProperties = new ConcurrentHashMap<>(); + /** + * Set the list of supported file extensions. + *

The default is a list containing {@code .properties} and {@code .xml}. + * @param fileExtensions the file extensions (starts with a dot) + * @since 6.1.0 + */ + public void setFileExtensions(List fileExtensions) { + Assert.isTrue(!CollectionUtils.isEmpty(fileExtensions), "At least one file extension is required"); + for (String extension : fileExtensions) { + if (!extension.startsWith(".")) { + throw new IllegalArgumentException("File extension '" + extension + "' should start with '.'"); + } + } + this.fileExtensions = Collections.unmodifiableList(fileExtensions); + } + /** * Set per-file charsets to use for parsing properties files. *

Only applies to classic properties files, not to XML files. @@ -471,9 +492,9 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased * JSON. *

The default implementation delegates to the configured * {@link #setResourceLoader(ResourceLoader) ResourceLoader} to resolve - * resources, first checking for an existing {@code Resource} with a - * {@code .properties} extension, and otherwise returning a {@code Resource} - * with a {@code .xml} extension. + * resources, checking in order for existing {@code Resource} with extensions defined + * by {@link #setFileExtensions(List)} ({@code .properties} and {@code .xml} + * by default). *

When overriding this method, {@link #loadProperties(Resource, String)} * must be capable of loading properties from any type of * {@code Resource} returned by this method. As a consequence, implementors @@ -491,11 +512,14 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased * @since 6.1 */ protected Resource resolveResource(String filename) { - Resource propertiesResource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); - if (propertiesResource.exists()) { - return propertiesResource; + Resource resource = null; + for (String fileExtension : this.fileExtensions) { + resource = this.resourceLoader.getResource(filename + fileExtension); + if (resource.exists()) { + return resource; + } } - return this.resourceLoader.getResource(filename + XML_SUFFIX); + return Objects.requireNonNull(resource); } /** @@ -509,7 +533,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased Properties props = newProperties(); try (InputStream is = resource.getInputStream()) { String resourceFilename = resource.getFilename(); - if (resourceFilename != null && resourceFilename.endsWith(XML_SUFFIX)) { + if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) { if (logger.isDebugEnabled()) { logger.debug("Loading properties [" + resource.getFilename() + "]"); } diff --git a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java index d799515796d..063a43aea71 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Properties; @@ -31,9 +32,11 @@ import org.springframework.context.i18n.LocaleContextHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 03.02.2004 */ class ResourceBundleMessageSourceTests { @@ -417,6 +420,31 @@ class ResourceBundleMessageSourceTests { assertThat(filenames).isEmpty(); } + @Test + void reloadableResourceBundleMessageSourceWithCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setFileExtensions(List.of(".toskip", ".custom")); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithEmptyCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + assertThatThrownBy(() -> ms.setFileExtensions(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one file extension is required"); + } + + @Test + void reloadableResourceBundleMessageSourceWithInvalidCustomFileExtensions() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + assertThatThrownBy(() -> ms.setFileExtensions(List.of("invalid"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("File extension 'invalid' should start with '.'"); + } + @Test void messageSourceResourceBundle() { ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages.custom b/spring-context/src/test/resources/org/springframework/context/support/messages.custom new file mode 100644 index 00000000000..714793d2a36 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages.custom @@ -0,0 +1,2 @@ +code1=message1 +code2=message2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom b/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom new file mode 100644 index 00000000000..a9a00b17a0d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de.custom @@ -0,0 +1 @@ +code2=nachricht2