diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index d671304708..c36d46a675 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-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. @@ -26,12 +26,16 @@ import java.util.List; import java.util.stream.Collectors; import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaCall; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClass.Predicates; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaParameter; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.core.domain.properties.HasName; +import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With; +import com.tngtech.archunit.core.domain.properties.HasParameterTypes; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; @@ -58,11 +62,14 @@ import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SkipWhenEmpty; import org.gradle.api.tasks.TaskAction; +import org.springframework.util.ResourceUtils; + /** * {@link Task} that checks for architecture problems. * * @author Andy Wilkinson * @author Yanming Zhou + * @author Scott Frederick */ public abstract class ArchitectureCheck extends DefaultTask { @@ -75,7 +82,8 @@ public abstract class ArchitectureCheck extends DefaultTask { allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(), noClassesShouldCallStepVerifierStepVerifyComplete(), noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(), - noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding()); + noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(), + noClassesShouldLoadResourcesUsingResourceUtils()); getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList())); } @@ -208,6 +216,18 @@ public abstract class ArchitectureCheck extends DefaultTask { .because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead"); } + private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() { + return ArchRuleDefinition.noClasses() + .should() + .callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) + .and(JavaCall.Predicates.target(HasName.Predicates.name("getURL"))) + .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))) + .or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) + .and(JavaCall.Predicates.target(HasName.Predicates.name("getFile"))) + .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))))) + .because("org.springframework.boot.io.ApplicationResourceLoader should be used instead"); + } + public void setClasses(FileCollection classes) { this.classes = classes; } diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index 1294d61929..085db6c66e 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * Tests for {@link ArchitectureCheck}. * * @author Andy Wilkinson + * @author Scott Frederick */ class ArchitectureCheckTests { @@ -121,6 +122,22 @@ class ArchitectureCheckTests { }); } + @Test + void whenClassLoadsResourceUsingResourceUtilsTaskFailsAndWritesReport() throws Exception { + prepareTask("resources/loads", (architectureCheck) -> { + assertThatExceptionOfType(GradleException.class).isThrownBy(architectureCheck::checkArchitecture); + assertThat(failureReport(architectureCheck)).isNotEmpty(); + }); + } + + @Test + void whenClassUsesResourceUtilsWithoutLoadingResourcesTaskSucceedsAndWritesAnEmptyReport() throws Exception { + prepareTask("resources/noloads", (architectureCheck) -> { + architectureCheck.checkArchitecture(); + assertThat(failureReport(architectureCheck)).isEmpty(); + }); + } + private void prepareTask(String classes, Callback callback) throws Exception { File projectDir = new File(this.temp, "project"); projectDir.mkdirs(); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/loads/ResourceUtilsResourceLoader.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/loads/ResourceUtilsResourceLoader.java new file mode 100644 index 0000000000..ce5ff3f61b --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/loads/ResourceUtilsResourceLoader.java @@ -0,0 +1,29 @@ +/* + * 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.build.architecture.resources.loads; + +import java.io.FileNotFoundException; + +import org.springframework.util.ResourceUtils; + +public class ResourceUtilsResourceLoader { + + void getResource() throws FileNotFoundException { + ResourceUtils.getURL("gradle.properties"); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/noloads/ResourceUtilsWithoutLoading.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/noloads/ResourceUtilsWithoutLoading.java new file mode 100644 index 0000000000..98d41edad5 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/resources/noloads/ResourceUtilsWithoutLoading.java @@ -0,0 +1,32 @@ +/* + * 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.build.architecture.resources.noloads; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.springframework.util.ResourceUtils; + +public class ResourceUtilsWithoutLoading { + + void inspectResourceLocation() throws MalformedURLException { + ResourceUtils.isUrl("gradle.properties"); + ResourceUtils.isFileURL(new URL("gradle.properties")); + "test".startsWith(ResourceUtils.FILE_URL_PREFIX); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java index 7be8e3eceb..248f88c86b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,13 +16,12 @@ package org.springframework.boot.autoconfigure.ssl; -import java.io.FileNotFoundException; -import java.net.URL; import java.nio.file.Path; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.pem.PemContent; +import org.springframework.core.io.Resource; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -57,9 +56,9 @@ record BundleContentProperty(String name, String value) { private Path toPath() { try { - URL url = toUrl(); - Assert.state(isFileUrl(url), () -> "Value '%s' is not a file URL".formatted(url)); - return Path.of(url.toURI()).toAbsolutePath(); + Resource resource = getResource(); + Assert.state(resource.isFile(), () -> "Value '%s' is not a file resource".formatted(this.value)); + return Path.of(resource.getFile().getAbsolutePath()); } catch (Exception ex) { throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name), @@ -67,13 +66,10 @@ record BundleContentProperty(String name, String value) { } } - private URL toUrl() throws FileNotFoundException { + private Resource getResource() { Assert.state(!isPemContent(), "Value contains PEM content"); - return ResourceUtils.getURL(this.value); - } - - private boolean isFileUrl(URL url) { - return "file".equalsIgnoreCase(url.getProtocol()); + ApplicationResourceLoader resourceLoader = new ApplicationResourceLoader(); + return resourceLoader.getResource(this.value); } } 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 72d5f6ae31..1087a980fc 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,10 +16,11 @@ package org.springframework.boot.autoconfigure.ssl; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Path; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -37,9 +38,6 @@ class BundleContentPropertyTests { -----END CERTIFICATE----- """; - @TempDir - Path temp; - @Test void isPemContentWhenValueIsPemTextReturnsTrue() { BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); @@ -78,8 +76,9 @@ class BundleContentPropertyTests { } @Test - void toWatchPathWhenPathReturnsPath() { - Path file = this.temp.toAbsolutePath().resolve("file.txt"); + void toWatchPathWhenPathReturnsPath() throws URISyntaxException { + URL resource = getClass().getResource("keystore.jks"); + Path file = Path.of(resource.toURI()).toAbsolutePath(); BundleContentProperty property = new BundleContentProperty("name", file.toString()); assertThat(property.toWatchPath()).isEqualTo(file); } 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 14a04ff335..03cc3dd00f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -19,36 +19,26 @@ package org.springframework.boot.convert; import java.io.File; import java.io.IOException; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.util.ResourceUtils; /** * {@link Converter} to convert from a {@link String} to a {@link File}. Supports basic * file conversion as well as file URLs. * * @author Phillip Webb + * @author Scott Frederick */ class StringToFileConverter implements Converter { - private static final ResourceLoader resourceLoader = new DefaultResourceLoader(null); + private static final ResourceLoader resourceLoader = new ApplicationResourceLoader(); @Override public File convert(String source) { - if (ResourceUtils.isUrl(source)) { - return getFile(resourceLoader.getResource(source)); - } - File file = new File(source); - if (file.exists()) { - return file; - } Resource resource = resourceLoader.getResource(source); - if (resource.exists()) { - return getFile(resource); - } - return file; + return getFile(resource); } 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 new file mode 100644 index 0000000000..48fe3ff59c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java @@ -0,0 +1,73 @@ +/* + * 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 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; + +/** + * A {@link DefaultResourceLoader} with any {@link ProtocolResolver}s 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 {@code DefaultResourceLoader}, + * which resolves unqualified paths to classpath resources. + * + * @author Scott Frederick + * @since 3.3.0 + */ +public class ApplicationResourceLoader extends DefaultResourceLoader { + + /** + * Create a new {@code ApplicationResourceLoader}. + */ + public ApplicationResourceLoader() { + super(); + ProtocolResolvers.applyTo(this); + } + + /** + * Create a new {@code ApplicationResourceLoader}. + * @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 + */ + public ApplicationResourceLoader(ClassLoader classLoader) { + super(classLoader); + ProtocolResolvers.applyTo(this); + } + + @Override + protected Resource getResourceByPath(String path) { + return new FileSystemContextResource(path); + } + + private static class FileSystemContextResource extends FileSystemResource implements ContextResource { + + FileSystemContextResource(String path) { + super(path); + } + + @Override + public String getPathWithinContext() { + return getPath(); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/Base64ProtocolResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/Base64ProtocolResolver.java new file mode 100644 index 0000000000..f2bc4811e9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/Base64ProtocolResolver.java @@ -0,0 +1,51 @@ +/* + * 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.util.Base64; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * {@link ProtocolResolver} for resources containing base 64 encoded text. + * + * @author Scott Frederick + */ +class Base64ProtocolResolver implements ProtocolResolver { + + private static final String BASE64_PREFIX = "base64:"; + + @Override + public Resource resolve(String location, ResourceLoader resourceLoader) { + if (location.startsWith(BASE64_PREFIX)) { + return new Base64ByteArrayResource(location.substring(BASE64_PREFIX.length())); + } + return null; + } + + static class Base64ByteArrayResource extends ByteArrayResource { + + Base64ByteArrayResource(String location) { + super(Base64.getDecoder().decode(location.getBytes())); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolvers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolvers.java new file mode 100644 index 0000000000..d59ee28297 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ProtocolResolvers.java @@ -0,0 +1,45 @@ +/* + * 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.util.List; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.Assert; + +/** + * {@link ProtocolResolver} implementations that are loaded from a + * {@code spring.factories} file. + * + * @author Scott Frederick + */ +final class ProtocolResolvers { + + private ProtocolResolvers() { + } + + static void applyTo(T resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + SpringFactoriesLoader loader = SpringFactoriesLoader + .forDefaultResourceLocation(resourceLoader.getClassLoader()); + List resolvers = loader.load(ProtocolResolver.class); + resourceLoader.getProtocolResolvers().addAll(resolvers); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/package-info.java new file mode 100644 index 0000000000..c06e30d917 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support for loading resources. + */ +package org.springframework.boot.io; 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 c28837788e..4afb84f173 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.logging.AbstractLoggingSystem; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; @@ -37,10 +38,10 @@ import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.FileCopyUtils; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -102,8 +103,8 @@ public class JavaLoggingSystem extends AbstractLoggingSystem { protected void loadConfiguration(String location, LogFile logFile) { Assert.notNull(location, "Location must not be null"); try { - String configuration = FileCopyUtils - .copyToString(new InputStreamReader(ResourceUtils.getURL(location).openStream())); + Resource resource = new ApplicationResourceLoader().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 10152f88c3..4500b4b132 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -55,6 +55,7 @@ import org.apache.logging.log4j.util.PropertiesUtil; import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.logging.AbstractLoggingSystem; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; @@ -67,10 +68,10 @@ import org.springframework.core.Conventions; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -300,15 +301,16 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } private Configuration load(String location, LoggerContext context) throws IOException { - URL url = ResourceUtils.getURL(location); - ConfigurationSource source = getConfigurationSource(url); + Resource resource = new ApplicationResourceLoader().getResource(location); + ConfigurationSource source = getConfigurationSource(resource); return ConfigurationFactory.getInstance().getConfiguration(context, source); } - private ConfigurationSource getConfigurationSource(URL url) throws IOException { - if (FILE_PROTOCOL.equals(url.getProtocol())) { - return new ConfigurationSource(url.openStream(), ResourceUtils.getFile(url)); + private ConfigurationSource getConfigurationSource(Resource resource) throws IOException { + if (resource.isFile()) { + return new ConfigurationSource(resource.getInputStream(), resource.getFile()); } + URL url = resource.getURL(); AuthorizationProvider authorizationProvider = ConfigurationFactory .authorizationProvider(PropertiesUtil.getProperties()); SslConfiguration sslConfiguration = url.getProtocol().equals("https") 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 009da77db7..e5bddb3ab5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -50,6 +50,7 @@ import org.springframework.aot.AotDetector; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.logging.AbstractLoggingSystem; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; @@ -62,9 +63,9 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -246,7 +247,8 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF applySystemProperties(initializationContext.getEnvironment(), logFile); } try { - configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location)); + Resource resource = new ApplicationResourceLoader().getResource(location); + configureByResourceUrl(initializationContext, loggerContext, resource.getURL()); } catch (Exception ex) { throw new IllegalStateException("Could not initialize Logback logging from " + location, 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 8f475f1c83..e3427ba3d8 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 @@ -18,17 +18,18 @@ package org.springframework.boot.ssl.jks; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.cert.CertificateException; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -114,8 +115,9 @@ public class JksSslStoreBundle implements SslStoreBundle { private void loadKeyStore(KeyStore store, String location, char[] password) { Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null"); try { - URL url = ResourceUtils.getURL(location); - try (InputStream stream = url.openStream()) { + ResourceLoader resourceLoader = new ApplicationResourceLoader(); + Resource resource = resourceLoader.getResource(location); + try (InputStream stream = resource.getInputStream()) { store.load(stream, password); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index d3013bcb6f..7e1f571e9d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -19,7 +19,6 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -30,8 +29,9 @@ import java.util.List; import java.util.Objects; import java.util.regex.Pattern; +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.Resource; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; /** @@ -119,7 +119,9 @@ public final class PemContent { return new PemContent(content); } try { - return load(ResourceUtils.getURL(content)); + ApplicationResourceLoader resourceLoader = new ApplicationResourceLoader(); + Resource resource = resourceLoader.getResource(content); + return load(resource.getInputStream()); } catch (IOException | UncheckedIOException ex) { throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex); @@ -139,13 +141,6 @@ public final class PemContent { } } - private static PemContent load(URL url) throws IOException { - Assert.notNull(url, "Url must not be null"); - try (InputStream in = url.openStream()) { - return load(in); - } - } - private static PemContent load(InputStream in) throws IOException { return of(StreamUtils.copyToString(in, StandardCharsets.UTF_8)); } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 9cf4aa5ca7..9a4ec6e1e7 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -101,3 +101,7 @@ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitiali org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\ org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\ org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector + +# Resource Locator Protocol Resolvers +org.springframework.core.io.ProtocolResolver=\ +org.springframework.boot.io.Base64ProtocolResolver diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToFileConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToFileConverterTests.java index e053f15bc1..a3cb2b37af 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToFileConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToFileConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,12 +17,14 @@ package org.springframework.boot.convert; import java.io.File; +import java.io.IOException; import java.util.stream.Stream; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.provider.Arguments; import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link StringToFileConverter}. * * @author Phillip Webb + * @author Scott Frederick */ class StringToFileConverterTests { @@ -48,6 +51,13 @@ class StringToFileConverterTests { .isEqualTo(new File(this.temp, "test").getAbsoluteFile()); } + @ConversionServiceTest + void convertWhenClasspathPrefixedReturnsFile(ConversionService conversionService) throws IOException { + String resource = new ClassPathResource("test-banner.txt", this.getClass().getClassLoader()).getURL().getFile(); + assertThat(convert(conversionService, "classpath:test-banner.txt").getAbsoluteFile()) + .isEqualTo(new File(resource).getAbsoluteFile()); + } + private File convert(ConversionService conversionService, String source) { return conversionService.convert(source, File.class); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/Base64ProtocolResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/Base64ProtocolResolverTests.java new file mode 100644 index 0000000000..606d4c3563 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/Base64ProtocolResolverTests.java @@ -0,0 +1,60 @@ +/* + * 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.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Base64ProtocolResolver}. + * + * @author Scott Frederick + */ +class Base64ProtocolResolverTests { + + @Test + void base64LocationResolves() throws IOException { + String location = Base64.getEncoder().encodeToString("test value".getBytes()); + Resource resource = new Base64ProtocolResolver().resolve("base64:" + location, new DefaultResourceLoader()); + assertThat(resource).isNotNull(); + assertThat(resource.getContentAsString(StandardCharsets.UTF_8)).isEqualTo("test value"); + } + + @Test + void base64LocationWithInvalidBase64ThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new Base64ProtocolResolver().resolve("base64:not valid base64", new DefaultResourceLoader())) + .withMessageContaining("Illegal base64"); + } + + @Test + void locationWithoutPrefixDoesNotResolve() { + Resource resource = new Base64ProtocolResolver().resolve("file:notbase64.txt", new DefaultResourceLoader()); + assertThat(resource).isNull(); + } + +} 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 159e98c456..cbbd14a715 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,12 +16,17 @@ package org.springframework.boot.ssl.jks; +import java.io.IOException; +import java.nio.file.Files; import java.security.KeyStore; +import java.util.Base64; import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.web.embedded.test.MockPkcs11Security; +import org.springframework.core.io.Resource; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; @@ -122,6 +127,36 @@ class JksSslStoreBundleTests { }).withMessageContaining("com.example.KeyStoreProvider"); } + @Test + void whenLocationsAreBase64Encoded() throws IOException { + JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(encodeFileContent("classpath:test.p12")) + .withPassword("secret"); + JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation(encodeFileContent("classpath:test.jks")) + .withPassword("secret"); + JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "secret")); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias", "password")); + } + + @Test + void invalidBase64EncodedLocationThrowsException() { + JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("base64:not base 64"); + assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(keyStoreDetails, null)) + .withMessageContaining("key store") + .withMessageContaining("base64:not base 64") + .havingRootCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Illegal base64"); + } + + @Test + void invalidLocationThrowsException() { + JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("does-not-exist.p12"); + assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(null, trustStoreDetails)) + .withMessageContaining("trust store") + .withMessageContaining("does-not-exist.p12"); + } + private Consumer storeContainingCertAndKey(String keyAlias, String keyPassword) { return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword); } @@ -136,4 +171,10 @@ class JksSslStoreBundleTests { }); } + private String encodeFileContent(String location) throws IOException { + Resource resource = new ApplicationResourceLoader().getResource(location); + byte[] bytes = Files.readAllBytes(resource.getFile().toPath()); + return "base64:" + Base64.getEncoder().encodeToString(bytes); + } + }