From e6b840004def5f96d095d6034593b59619567346 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Oct 2024 18:44:40 -0700 Subject: [PATCH] Support ResourceLoader delegation from ApplicationResourceLoader Update `ApplicationResourceLoader` to support delegation to another `ResourceLoader`. The update allows customer resource loaders to be used when loading SSL resources. Closes gh-42835 --- .../couchbase/CouchbaseAutoConfiguration.java | 8 +- .../ssl/PropertiesSslBundle.java | 4 +- .../ssl/SslAutoConfiguration.java | 4 +- .../ssl/BundleContentPropertyTests.java | 8 +- .../boot/convert/StringToFileConverter.java | 3 +- .../boot/io/ApplicationResourceLoader.java | 155 +++++++++++++- ...ResolverApplicationContextInitializer.java | 4 +- .../boot/logging/java/JavaLoggingSystem.java | 2 +- .../logging/log4j2/Log4J2LoggingSystem.java | 2 +- .../logging/logback/LogbackLoggingSystem.java | 2 +- .../boot/ssl/jks/JksSslStoreBundle.java | 2 +- .../boot/ssl/pem/PemSslStore.java | 2 +- .../io/ApplicationResourceLoaderTests.java | 201 ++++++++++++++++++ ...ionContextInitializerIntegrationTests.java | 2 +- .../io/ReverseStringProtocolResolver.java | 47 ++++ .../boot/ssl/jks/JksSslStoreBundleTests.java | 2 +- .../boot/ssl/pem/LoadedPemSslStoreTests.java | 8 +- .../boot/ssl/pem/PemContentTests.java | 16 +- .../spring-test-protocol-resolvers.factories | 2 + 19 files changed, 435 insertions(+), 39 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ReverseStringProtocolResolver.java create mode 100644 spring-boot-project/spring-boot/src/test/resources/META-INF/spring-test-protocol-resolvers.factories diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java index 20a7d7899a..b935fbca31 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java @@ -60,6 +60,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -81,9 +82,12 @@ import org.springframework.util.StringUtils; @EnableConfigurationProperties(CouchbaseProperties.class) public class CouchbaseAutoConfiguration { + private final ResourceLoader resourceLoader; + private final CouchbaseProperties properties; - CouchbaseAutoConfiguration(CouchbaseProperties properties) { + CouchbaseAutoConfiguration(ResourceLoader resourceLoader, CouchbaseProperties properties) { + this.resourceLoader = ApplicationResourceLoader.get(resourceLoader); this.properties = properties; } @@ -117,7 +121,7 @@ public class CouchbaseAutoConfiguration { } Jks jks = this.properties.getAuthentication().getJks(); if (jks.getLocation() != null) { - Resource resource = new ApplicationResourceLoader().getResource(jks.getLocation()); + Resource resource = this.resourceLoader.getResource(jks.getLocation()); String keystorePassword = jks.getPassword(); try (InputStream inputStream = resource.getInputStream()) { KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index e56ab89016..1a8a35aa4e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -99,7 +99,7 @@ public final class PropertiesSslBundle implements SslBundle { * @return an {@link SslBundle} instance */ public static SslBundle get(PemSslBundleProperties properties) { - return get(properties, new ApplicationResourceLoader()); + return get(properties, ApplicationResourceLoader.get()); } /** @@ -143,7 +143,7 @@ public final class PropertiesSslBundle implements SslBundle { * @return an {@link SslBundle} instance */ public static SslBundle get(JksSslBundleProperties properties) { - return get(properties, new ApplicationResourceLoader()); + return get(properties, ApplicationResourceLoader.get()); } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java index c53356a71e..46af4615a0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java @@ -38,12 +38,12 @@ import org.springframework.core.io.ResourceLoader; @EnableConfigurationProperties(SslProperties.class) public class SslAutoConfiguration { - private final ApplicationResourceLoader resourceLoader; + private final ResourceLoader resourceLoader; private final SslProperties sslProperties; SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) { - this.resourceLoader = new ApplicationResourceLoader(resourceLoader.getClassLoader()); + this.resourceLoader = ApplicationResourceLoader.get(resourceLoader); this.sslProperties = sslProperties; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java index b4e6477f00..c03165b708 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java @@ -78,7 +78,7 @@ class BundleContentPropertyTests { @Test void toWatchPathWhenNotPathThrowsException() { BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); - assertThatIllegalStateException().isThrownBy(() -> property.toWatchPath(new ApplicationResourceLoader())) + assertThatIllegalStateException().isThrownBy(() -> property.toWatchPath(ApplicationResourceLoader.get())) .withMessage("Unable to convert value of property 'name' to a path"); } @@ -87,7 +87,7 @@ class BundleContentPropertyTests { URL resource = getClass().getResource("keystore.jks"); Path file = Path.of(resource.toURI()).toAbsolutePath(); BundleContentProperty property = new BundleContentProperty("name", file.toString()); - assertThat(property.toWatchPath(new ApplicationResourceLoader())).isEqualTo(file); + assertThat(property.toWatchPath(ApplicationResourceLoader.get())).isEqualTo(file); } @Test @@ -95,7 +95,7 @@ class BundleContentPropertyTests { URL resource = getClass().getResource("keystore.jks"); Path file = Path.of(resource.toURI()).toAbsolutePath(); BundleContentProperty property = new BundleContentProperty("name", file.toString()); - ResourceLoader resourceLoader = spy(new ApplicationResourceLoader()); + ResourceLoader resourceLoader = spy(ApplicationResourceLoader.get()); assertThat(property.toWatchPath(resourceLoader)).isEqualTo(file); then(resourceLoader).should(atLeastOnce()).getResource(file.toString()); } @@ -104,7 +104,7 @@ class BundleContentPropertyTests { void shouldThrowBundleContentNotWatchableExceptionIfContentIsNotWatchable() { BundleContentProperty property = new BundleContentProperty("name", "https://example.com/"); assertThatExceptionOfType(BundleContentNotWatchableException.class) - .isThrownBy(() -> property.toWatchPath(new ApplicationResourceLoader())) + .isThrownBy(() -> property.toWatchPath(ApplicationResourceLoader.get())) .withMessageContaining("Only 'file:' resources are watchable"); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToFileConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToFileConverter.java index b8390526db..3757263433 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToFileConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToFileConverter.java @@ -34,8 +34,7 @@ class StringToFileConverter implements Converter { @Override public File convert(String source) { - Resource resource = new ApplicationResourceLoader().getResource(source); - return getFile(resource); + return getFile(ApplicationResourceLoader.get().getResource(source)); } private File getFile(Resource resource) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java index 369d06981f..819b082a99 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java @@ -16,27 +16,38 @@ package org.springframework.boot.io; +import java.util.List; + import org.springframework.core.io.ContextResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** - * A {@link DefaultResourceLoader} with any {@link ProtocolResolver ProtocolResolvers} - * registered in a {@code spring.factories} file applied to it. Plain paths without a - * qualifier will resolve to file system resources. This is different from + * Class can be used to obtain {@link ResourceLoader ResourceLoaders} supporting + * additional {@link ProtocolResolver ProtocolResolvers} registered in + * {@code spring.factories}. + *

+ * When not delegating to an existing resource loader, plain paths without a qualifier + * will resolve to file system resources. This is different from * {@code DefaultResourceLoader}, which resolves unqualified paths to classpath resources. * * @author Scott Frederick + * @author Phillip Webb * @since 3.3.0 */ public class ApplicationResourceLoader extends DefaultResourceLoader { /** * Create a new {@code ApplicationResourceLoader}. + * @deprecated since 3.4.0 for removal in 3.6.0 in favor of {@link #get()} */ + @Deprecated(since = "3.4.0", forRemoval = true) public ApplicationResourceLoader() { this(null); } @@ -46,7 +57,9 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { * @param classLoader the {@link ClassLoader} to load class path resources with, or * {@code null} for using the thread context class loader at the time of actual * resource access + * @deprecated since 3.4.0 for removal in 3.6.0 in favor of {@link #get(ClassLoader)} */ + @Deprecated(since = "3.4.0", forRemoval = true) public ApplicationResourceLoader(ClassLoader classLoader) { super(classLoader); SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(classLoader); @@ -55,12 +68,107 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { @Override protected Resource getResourceByPath(String path) { - return new FileSystemContextResource(path); + return new ApplicationResource(path); } - private static class FileSystemContextResource extends FileSystemResource implements ContextResource { + /** + * Return a {@link ResourceLoader} supporting additional {@link ProtocolResolver + * ProtocolResolvers} registered in {@code spring.factories}. The factories file will + * be resolved using the default class loader at the time this call is made. Resources + * will be resolved using the default class loader at the time they are resolved. + * @return a {@link ResourceLoader} instance + * @since 3.4.0 + */ + public static ResourceLoader get() { + return get((ClassLoader) null); + } - FileSystemContextResource(String path) { + /** + * Return a {@link ResourceLoader} supporting additional {@link ProtocolResolver + * ProtocolResolvers} registered in {@code spring.factories}. The factories files and + * resources will be resolved using the specified class loader. + * @param classLoader the class loader to use or {@code null} to use the default class + * loader + * @return a {@link ResourceLoader} instance + * @since 3.4.0 + */ + public static ResourceLoader get(ClassLoader classLoader) { + return get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader)); + } + + /** + * Return a {@link ResourceLoader} supporting additional {@link ProtocolResolver + * ProtocolResolvers} registered in {@code spring.factories}. + * @param classLoader the class loader to use or {@code null} to use the default class + * loader + * @param springFactoriesLoader the {@link SpringFactoriesLoader} used to load + * {@link ProtocolResolver ProtocolResolvers} + * @return a {@link ResourceLoader} instance + * @since 3.4.0 + */ + public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader) { + return get(ApplicationFileSystemResourceLoader.get(classLoader), springFactoriesLoader); + } + + /** + * Return a {@link ResourceLoader} delegating to the given resource loader and + * supporting additional {@link ProtocolResolver ProtocolResolvers} registered in + * {@code spring.factories}. The factories file will be resolved using the default + * class loader at the time this call is made. + * @param resourceLoader the delegate resource loader + * @return a {@link ResourceLoader} instance + * @since 3.4.0 + */ + public static ResourceLoader get(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); + return get(resourceLoader, SpringFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader())); + } + + /** + * Return a {@link ResourceLoader} delegating to the given resource loader and + * supporting additional {@link ProtocolResolver ProtocolResolvers} registered in + * {@code spring.factories}. + * @param resourceLoader the delegate resource loader + * @param springFactoriesLoader the {@link SpringFactoriesLoader} used to load + * {@link ProtocolResolver ProtocolResolvers} + * @return a {@link ResourceLoader} instance + * @since 3.4.0 + */ + public static ResourceLoader get(ResourceLoader resourceLoader, SpringFactoriesLoader springFactoriesLoader) { + Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); + Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null"); + return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class)); + } + + /** + * Internal {@link ResourceLoader} used to load {@link ApplicationResource}. + */ + private static final class ApplicationFileSystemResourceLoader extends DefaultResourceLoader { + + private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null); + + private ApplicationFileSystemResourceLoader(ClassLoader classLoader) { + super(classLoader); + } + + @Override + protected Resource getResourceByPath(String path) { + return new ApplicationResource(path); + } + + static ResourceLoader get(ClassLoader classLoader) { + return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader) + : ApplicationFileSystemResourceLoader.shared; + } + + } + + /** + * An application {@link Resource}. + */ + private static final class ApplicationResource extends FileSystemResource implements ContextResource { + + ApplicationResource(String path) { super(path); } @@ -71,4 +179,39 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { } + /** + * {@link ResourceLoader} decorator that adds support for additional + * {@link ProtocolResolver ProtocolResolvers}. + */ + private static class ProtocolResolvingResourceLoader implements ResourceLoader { + + private final ResourceLoader resourceLoader; + + private final List protocolResolvers; + + ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List protocolResolvers) { + this.resourceLoader = resourceLoader; + this.protocolResolvers = protocolResolvers; + } + + @Override + public Resource getResource(String location) { + if (StringUtils.hasLength(location)) { + for (ProtocolResolver protocolResolver : this.protocolResolvers) { + Resource resource = protocolResolver.resolve(location, this); + if (resource != null) { + return resource; + } + } + } + return this.resourceLoader.getResource(location); + } + + @Override + public ClassLoader getClassLoader() { + return this.resourceLoader.getClassLoader(); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializer.java index 615dcee9f3..95154b65b5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializer.java @@ -24,8 +24,8 @@ import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.support.SpringFactoriesLoader; /** - * {@link ApplicationContextInitializer} that adds all {@link ProtocolResolver}s - * registered in a {@code spring.factories} file. + * {@link ApplicationContextInitializer} that adds all {@link ProtocolResolver + * ProtocolResolvers} registered in a {@code spring.factories} file. * * @author Scott Frederick */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java index 4afb84f173..7fb5db460b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/JavaLoggingSystem.java @@ -103,7 +103,7 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { protected void loadConfiguration(String location, LogFile logFile) { Assert.notNull(location, "Location must not be null"); try { - Resource resource = new ApplicationResourceLoader().getResource(location); + Resource resource = ApplicationResourceLoader.get().getResource(location); String configuration = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); if (logFile != null) { configuration = configuration.replace("${LOG_FILE}", StringUtils.cleanPath(logFile.toString())); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 694c8f5cba..355db15256 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -275,7 +275,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } private Configuration load(String location, LoggerContext context) throws IOException { - Resource resource = new ApplicationResourceLoader().getResource(location); + Resource resource = ApplicationResourceLoader.get().getResource(location); ConfigurationSource source = getConfigurationSource(resource); return ConfigurationFactory.getInstance().getConfiguration(context, source); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index 79dfff0370..332b92d92b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -258,7 +258,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF applySystemProperties(initializationContext.getEnvironment(), logFile); } try { - Resource resource = new ApplicationResourceLoader().getResource(location); + Resource resource = ApplicationResourceLoader.get().getResource(location); configureByResourceUrl(initializationContext, loggerContext, resource.getURL()); } catch (Exception ex) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java index 7bc6b5f78d..12835300c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java @@ -57,7 +57,7 @@ public class JksSslStoreBundle implements SslStoreBundle { * @param trustStoreDetails the trust store details */ public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) { - this(keyStoreDetails, trustStoreDetails, new ApplicationResourceLoader()); + this(keyStoreDetails, trustStoreDetails, ApplicationResourceLoader.get()); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java index 7081aba6d1..5223e9a410 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -95,7 +95,7 @@ public interface PemSslStore { * @return a loaded {@link PemSslStore} or {@code null}. */ static PemSslStore load(PemSslStoreDetails details) { - return load(details, new ApplicationResourceLoader()); + return load(details, ApplicationResourceLoader.get()); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java new file mode 100644 index 0000000000..0384f7cfcc --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java @@ -0,0 +1,201 @@ +/* + * Copyright 2012-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.io; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Enumeration; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ApplicationResourceLoader}. + * + * @author Phillip Webb + */ +class ApplicationResourceLoaderTests { + + private static final String SPRING_FACTORIES = "META-INF/spring.factories"; + + private static final String TEST_PROTOCOL_RESOLVERS_FACTORIES = "META-INF/spring-test-protocol-resolvers.factories"; + + private static final String TEST_BASE_64_VALUE = Base64.getEncoder().encodeToString("test".getBytes()); + + @Test + void getIncludesProtocolResolvers() throws IOException { + ResourceLoader loader = ApplicationResourceLoader.get(); + Resource resource = loader.getResource("base64:" + TEST_BASE_64_VALUE); + assertThat(contentAsString(resource)).isEqualTo("test"); + } + + @Test + void getWithClassPathIncludesProtocolResolvers() throws IOException { + ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories); + ResourceLoader loader = ApplicationResourceLoader.get(classLoader); + Resource resource = loader.getResource("reverse:test"); + assertThat(contentAsString(resource)).isEqualTo("tset"); + } + + @Test + void getWithClassPathWhenClassPathIsNullIncludesProtocolResolvers() throws IOException { + ResourceLoader loader = ApplicationResourceLoader.get((ClassLoader) null); + Resource resource = loader.getResource("base64:" + TEST_BASE_64_VALUE); + assertThat(contentAsString(resource)).isEqualTo("test"); + } + + @Test + void getWithClassPathAndSpringFactoriesLoaderIncludesProtocolResolvers() throws IOException { + SpringFactoriesLoader springFactoriesLoader = SpringFactoriesLoader + .forResourceLocation(TEST_PROTOCOL_RESOLVERS_FACTORIES); + ResourceLoader loader = ApplicationResourceLoader.get((ClassLoader) null, springFactoriesLoader); + Resource resource = loader.getResource("reverse:test"); + assertThat(contentAsString(resource)).isEqualTo("tset"); + } + + @Test + void getWithClassPathAndSpringFactoriesLoaderWhenSpringFactoriesLoaderIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApplicationResourceLoader.get((ClassLoader) null, null)) + .withMessage("'springFactoriesLoader' must not be null"); + } + + @Test + void getWithResourceLoaderIncludesProtocolResolvers() throws IOException { + ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader()); + Resource resource = loader.getResource("base64:" + TEST_BASE_64_VALUE); + assertThat(contentAsString(resource)).isEqualTo("test"); + } + + @Test + void getWithResourceLoaderDelegatesLoading() throws IOException { + DefaultResourceLoader delegate = new TestResourceLoader(); + ResourceLoader loader = ApplicationResourceLoader.get(delegate); + assertThat(contentAsString(loader.getResource("spring"))).isEqualTo("boot"); + } + + @Test + void getWithResourceLoaderWhenResourceLoaderIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApplicationResourceLoader.get((ResourceLoader) null)) + .withMessage("'resourceLoader' must not be null"); + } + + @Test + void getWithResourceLoaderAndSpringFactoriesLoaderIncludesProtocolResolvers() throws IOException { + DefaultResourceLoader delegate = new TestResourceLoader(); + ResourceLoader loader = ApplicationResourceLoader.get(delegate); + Resource resource = loader.getResource("base64:" + TEST_BASE_64_VALUE); + assertThat(contentAsString(resource)).isEqualTo("test"); + } + + @Test + void getWithResourceLoaderAndSpringFactoriesLoaderWhenResourceLoaderIsNullThrowsException() { + SpringFactoriesLoader springFactoriesLoader = SpringFactoriesLoader + .forResourceLocation(TEST_PROTOCOL_RESOLVERS_FACTORIES); + assertThatIllegalArgumentException() + .isThrownBy(() -> ApplicationResourceLoader.get((ResourceLoader) null, springFactoriesLoader)) + .withMessage("'resourceLoader' must not be null"); + } + + @Test + void getWithResourceLoaderAndSpringFactoriesLoaderWhenSpringFactoriesLoaderIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ApplicationResourceLoader.get(new TestResourceLoader(), null)) + .withMessage("'springFactoriesLoader' must not be null"); + } + + @Test + void getResourceWhenPathIsRelative() throws IOException { + ResourceLoader loader = ApplicationResourceLoader.get(); + String name = "src/test/resources/" + TEST_PROTOCOL_RESOLVERS_FACTORIES; + Resource resource = loader.getResource(name); + assertThat(resource.getFile()).isEqualTo(new File(name)); + } + + @Test + void getResourceWhenPathIsAbsolute() throws IOException { + File file = new File("src/test/resources/" + TEST_PROTOCOL_RESOLVERS_FACTORIES); + ResourceLoader loader = ApplicationResourceLoader.get(); + Resource resource = loader.getResource(file.getAbsolutePath()); + assertThat(resource.getFile()).hasSameBinaryContentAs(file); + } + + @Test + void getResourceWhenPathIsNull() { + ResourceLoader loader = ApplicationResourceLoader.get(); + assertThatIllegalArgumentException().isThrownBy(() -> loader.getResource(null)) + .withMessage("Location must not be null"); + } + + @Test + void getClassLoaderReturnsDelegateClassLoader() { + ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories); + ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(classLoader)); + assertThat(loader.getClassLoader()).isSameAs(classLoader); + } + + private String contentAsString(Resource resource) throws IOException { + return resource.getContentAsString(StandardCharsets.UTF_8); + } + + private String useTestProtocolResolversFactories(String name) { + return (!SPRING_FACTORIES.equals(name)) ? name : TEST_PROTOCOL_RESOLVERS_FACTORIES; + } + + static class TestClassLoader extends ClassLoader { + + private final UnaryOperator mapper; + + TestClassLoader(UnaryOperator mapper) { + super(Thread.currentThread().getContextClassLoader()); + this.mapper = mapper; + } + + @Override + public URL getResource(String name) { + return super.getResource(this.mapper.apply(name)); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return super.getResources(this.mapper.apply(name)); + } + + } + + static class TestResourceLoader extends DefaultResourceLoader { + + @Override + public Resource getResource(String location) { + return (!"spring".equals(location)) ? super.getResource(location) + : new ByteArrayResource("boot".getBytes(StandardCharsets.UTF_8)); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializerIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializerIntegrationTests.java index 1673edbb58..25acae6d03 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializerIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ProtocolResolverApplicationContextInitializerIntegrationTests.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for resolving configuration properties using - * {@code ProtocolResolver}s. + * {@code ProtocolResolver ProtocolResolvers}. * * @author Scott Frederick */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ReverseStringProtocolResolver.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ReverseStringProtocolResolver.java new file mode 100644 index 0000000000..1f1f9ca1b4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ReverseStringProtocolResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.io; + +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * Test {@link ProtocolResolver} that reverses a String. + * + * @author Phillip Webb + */ +class ReverseStringProtocolResolver implements ProtocolResolver { + + private static final String PREFIX = "reverse:"; + + @Override + public Resource resolve(String location, ResourceLoader resourceLoader) { + if (!location.startsWith(PREFIX)) { + return null; + } + return new ByteArrayResource(reverse(location.substring(PREFIX.length()))); + } + + private byte[] reverse(String substring) { + return new StringBuilder(substring).reverse().toString().getBytes(StandardCharsets.UTF_8); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java index 2e6fd4b54e..8b41232abf 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java @@ -193,7 +193,7 @@ class JksSslStoreBundleTests { } private String encodeFileContent(String location) throws IOException { - Resource resource = new ApplicationResourceLoader().getResource(location); + Resource resource = ApplicationResourceLoader.get().getResource(location); byte[] bytes = Files.readAllBytes(resource.getFile().toPath()); return "base64:" + Base64.getEncoder().encodeToString(bytes); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java index c590a721ce..a188092a74 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java @@ -40,7 +40,7 @@ class LoadedPemSslStoreTests { void certificatesAreLoadedLazily() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") .withPrivateKey("classpath:test-key.pem"); - LoadedPemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()); + LoadedPemSslStore store = new LoadedPemSslStore(details, ApplicationResourceLoader.get()); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); } @@ -48,7 +48,7 @@ class LoadedPemSslStoreTests { void privateKeyIsLoadedLazily() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:missing-test-key.pem"); - LoadedPemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()); + LoadedPemSslStore store = new LoadedPemSslStore(details, ApplicationResourceLoader.get()); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey); } @@ -56,7 +56,7 @@ class LoadedPemSslStoreTests { void withAliasIsLazy() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") .withPrivateKey("classpath:test-key.pem"); - PemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()).withAlias("alias"); + PemSslStore store = new LoadedPemSslStore(details, ApplicationResourceLoader.get()).withAlias("alias"); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); } @@ -64,7 +64,7 @@ class LoadedPemSslStoreTests { void withPasswordIsLazy() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") .withPrivateKey("classpath:test-key.pem"); - PemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()).withPassword("password"); + PemSslStore store = new LoadedPemSslStore(details, ApplicationResourceLoader.get()).withPassword("password"); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index e999fc262b..f4894165df 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -53,7 +53,7 @@ class PemContentTests { @Test void getCertificateReturnsCertificates() throws Exception { PemContent content = PemContent.load(contentFromClasspath("/test-cert-chain.pem"), - new ApplicationResourceLoader()); + ApplicationResourceLoader.get()); List certificates = content.getCertificates(); assertThat(certificates).isNotNull(); assertThat(certificates).hasSize(2); @@ -71,7 +71,7 @@ class PemContentTests { @Test void getPrivateKeyReturnsPrivateKey() throws Exception { PemContent content = PemContent.load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key"), - new ApplicationResourceLoader()); + ApplicationResourceLoader.get()); PrivateKey privateKey = content.getPrivateKey(); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); @@ -95,7 +95,7 @@ class PemContentTests { @Test void loadWithStringWhenContentIsNullReturnsNull() throws Exception { - assertThat(PemContent.load((String) null, new ApplicationResourceLoader())).isNull(); + assertThat(PemContent.load((String) null, ApplicationResourceLoader.get())).isNull(); } @Test @@ -118,7 +118,7 @@ class PemContentTests { +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO 32C9XWHwRA4= -----END CERTIFICATE-----"""; - assertThat(PemContent.load(content, new ApplicationResourceLoader())).hasToString(content); + assertThat(PemContent.load(content, ApplicationResourceLoader.get())).hasToString(content); } @Test @@ -159,11 +159,11 @@ class PemContentTests { +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO 32C9XWHwRA4= -----END CERTIFICATE-----"""; - assertThat(PemContent.load(content, new ApplicationResourceLoader())).hasToString(trimmedContent); + assertThat(PemContent.load(content, ApplicationResourceLoader.get())).hasToString(trimmedContent); } @Test - void isPresentInTextWithUntrimmedContent() throws Exception { + void isPresentInTextWithUntrimmedContent() { String content = """ -----BEGIN CERTIFICATE----- MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls @@ -187,14 +187,14 @@ class PemContentTests { @Test void loadWithStringWhenClasspathLocationReturnsContent() throws IOException { - String actual = PemContent.load("classpath:test-cert.pem", new ApplicationResourceLoader()).toString(); + String actual = PemContent.load("classpath:test-cert.pem", ApplicationResourceLoader.get()).toString(); String expected = contentFromClasspath("test-cert.pem"); assertThat(actual).isEqualTo(expected); } @Test void loadWithStringWhenFileLocationReturnsContent() throws IOException { - String actual = PemContent.load("src/test/resources/test-cert.pem", new ApplicationResourceLoader()).toString(); + String actual = PemContent.load("src/test/resources/test-cert.pem", ApplicationResourceLoader.get()).toString(); String expected = contentFromClasspath("test-cert.pem"); assertThat(actual).isEqualTo(expected); } diff --git a/spring-boot-project/spring-boot/src/test/resources/META-INF/spring-test-protocol-resolvers.factories b/spring-boot-project/spring-boot/src/test/resources/META-INF/spring-test-protocol-resolvers.factories new file mode 100644 index 0000000000..79239c637b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/META-INF/spring-test-protocol-resolvers.factories @@ -0,0 +1,2 @@ +org.springframework.core.io.ProtocolResolver=\ +org.springframework.boot.io.ReverseStringProtocolResolver \ No newline at end of file