From 528b7e9ad919571bc04f3b4b2d0c557cf8ef00d6 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 11 Feb 2025 10:08:25 +0100 Subject: [PATCH] Add working directory support for ApplicationResourceLoader See gh-41137 --- .../boot/io/ApplicationResourceLoader.java | 71 +++++++++-- .../io/ApplicationResourceLoaderTests.java | 119 +++++++++++++++++- 2 files changed, 182 insertions(+), 8 deletions(-) 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 de160fdaa3..cd5af8f959 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -16,6 +16,11 @@ package org.springframework.boot.io; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.List; import org.springframework.core.io.ClassPathResource; @@ -40,6 +45,7 @@ import org.springframework.util.StringUtils; * {@code DefaultResourceLoader}, which resolves unqualified paths to classpath resources. * * @author Scott Frederick + * @author Moritz Halbritter * @author Phillip Webb * @since 3.3.0 */ @@ -109,7 +115,23 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { * @since 3.4.0 */ public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader) { - return get(ApplicationFileSystemResourceLoader.get(classLoader), springFactoriesLoader); + return get(classLoader, springFactoriesLoader, null); + } + + /** + * 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} + * @param workingDirectory the working directory + * @return a {@link ResourceLoader} instance + * @since 3.5.0 + */ + public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader springFactoriesLoader, + Path workingDirectory) { + return get(ApplicationFileSystemResourceLoader.get(classLoader, workingDirectory), springFactoriesLoader); } /** @@ -170,10 +192,41 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { */ private static final class ApplicationFileSystemResourceLoader extends DefaultResourceLoader { - private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null); + private static final ResourceLoader shared = new ApplicationFileSystemResourceLoader(null, null); - private ApplicationFileSystemResourceLoader(ClassLoader classLoader) { + private final Path workingDirectory; + + private ApplicationFileSystemResourceLoader(ClassLoader classLoader, Path workingDirectory) { super(classLoader); + this.workingDirectory = workingDirectory; + } + + @Override + public Resource getResource(String location) { + Resource resource = super.getResource(location); + if (this.workingDirectory == null) { + return resource; + } + if (!resource.isFile()) { + return resource; + } + return resolveFile(resource); + } + + private Resource resolveFile(Resource resource) { + try { + File file = resource.getFile(); + if (file.isAbsolute()) { + return resource; + } + return new ApplicationResource(new File(this.workingDirectory.toFile(), file.getPath()).getPath()); + } + catch (FileNotFoundException ex) { + return resource; + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } } @Override @@ -181,8 +234,12 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { return new ApplicationResource(path); } - static ResourceLoader get(ClassLoader classLoader) { - return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader) + static ResourceLoader get(ClassLoader classLoader, Path workingDirectory) { + if (classLoader == null && workingDirectory != null) { + throw new IllegalArgumentException( + "It's not possible to use null as 'classLoader' but specify a 'workingDirectory'"); + } + return (classLoader != null) ? new ApplicationFileSystemResourceLoader(classLoader, workingDirectory) : ApplicationFileSystemResourceLoader.shared; } @@ -218,7 +275,7 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { private final boolean preferFileResolution; - private Class servletContextResourceClass; + private final Class servletContextResourceClass; ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List protocolResolvers, boolean preferFileResolution) { 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 index 1241473295..c7e863cf41 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.Base64; import java.util.Enumeration; import java.util.function.UnaryOperator; @@ -45,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * Tests for {@link ApplicationResourceLoader}. * * @author Phillip Webb + * @author Moritz Halbritter */ class ApplicationResourceLoaderTests { @@ -61,6 +63,121 @@ class ApplicationResourceLoaderTests { assertThat(contentAsString(resource)).isEqualTo("test"); } + @Test + void shouldLoadAbsolutePath() throws IOException { + Resource resource = ApplicationResourceLoader.get().getResource("/root/file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/root").hasName("file.txt"); + } + + @Test + void shouldLoadAbsolutePathWithWorkingDirectory() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("/root/file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/root").hasName("file.txt"); + } + + @Test + void shouldLoadRelativeFilename() throws IOException { + Resource resource = ApplicationResourceLoader.get().getResource("file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasNoParent().hasName("file.txt"); + } + + @Test + void shouldLoadRelativeFilenameWithWorkingDirectory() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt"); + } + + @Test + void shouldLoadRelativePathWithWorkingDirectory() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("a/file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/working-directory/a").hasName("file.txt"); + } + + @Test + void shouldLoadClasspathLocations() { + Resource resource = ApplicationResourceLoader.get().getResource("classpath:a-file"); + assertThat(resource.exists()).isTrue(); + } + + @Test + void shouldLoadNonExistentClasspathLocations() { + Resource resource = ApplicationResourceLoader.get().getResource("classpath:doesnt-exist"); + assertThat(resource.exists()).isFalse(); + } + + @Test + void shouldLoadClasspathLocationsWithWorkingDirectory() { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("classpath:a-file"); + assertThat(resource.exists()).isTrue(); + } + + @Test + void shouldLoadNonExistentClasspathLocationsWithWorkingDirectory() { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("classpath:doesnt-exist"); + assertThat(resource.exists()).isFalse(); + } + + @Test + void shouldLoadRelativeFileUris() throws IOException { + Resource resource = ApplicationResourceLoader.get().getResource("file:file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasNoParent().hasName("file.txt"); + } + + @Test + void shouldLoadAbsoluteFileUris() throws IOException { + Resource resource = ApplicationResourceLoader.get().getResource("file:/file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/").hasName("file.txt"); + } + + @Test + void shouldLoadRelativeFileUrisWithWorkingDirectory() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("file:file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/working-directory").hasName("file.txt"); + } + + @Test + void shouldLoadAbsoluteFileUrisWithWorkingDirectory() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + Resource resource = ApplicationResourceLoader + .get(classLoader, SpringFactoriesLoader.forDefaultResourceLocation(classLoader), + Path.of("/working-directory")) + .getResource("file:/file.txt"); + assertThat(resource.isFile()).isTrue(); + assertThat(resource.getFile()).hasParent("/").hasName("file.txt"); + } + @Test void getWithClassPathIncludesProtocolResolvers() throws IOException { ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);