From aeb6537f57d00cdf47b948067c8a3421a71e72d8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 13:41:31 -0700 Subject: [PATCH 1/3] Rename spring-boot-loader to spring-boot-loader-classic Rename the `spring-boot-loader` module to `spring-boot-loader-classic` so that we can introduce an alternative loader implementation. See gh-37669 --- eclipse/spring-boot-project.setup | 2 +- settings.gradle | 4 ++-- .../spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-antlib/build.gradle | 2 +- .../org/springframework/boot/ant/antlib.xml | 4 ++-- .../spring-boot-tools/spring-boot-cli/build.gradle | 2 +- .../gradle/tasks/bundling/LoaderZipEntries.java | 2 +- .../spring-boot-jarmode-layertools/build.gradle | 2 +- .../build.gradle | 2 +- .../boot/loader/ClassPathIndexFile.java | 2 +- .../boot/loader/ExecutableArchiveLauncher.java | 2 +- .../springframework/boot/loader/JarLauncher.java | 2 +- .../boot/loader/LaunchedURLClassLoader.java | 2 +- .../org/springframework/boot/loader/Launcher.java | 2 +- .../boot/loader/MainMethodRunner.java | 2 +- .../boot/loader/PropertiesLauncher.java | 0 .../springframework/boot/loader/WarLauncher.java | 2 +- .../boot/loader/archive/Archive.java | 2 +- .../boot/loader/archive/ExplodedArchive.java | 2 +- .../boot/loader/archive/JarFileArchive.java | 2 +- .../boot/loader/archive/package-info.java | 2 +- .../boot/loader/data/RandomAccessData.java | 2 +- .../boot/loader/data/RandomAccessDataFile.java | 0 .../boot/loader/data/package-info.java | 2 +- .../boot/loader/jar/AbstractJarFile.java | 2 +- .../springframework/boot/loader/jar/AsciiBytes.java | 2 +- .../org/springframework/boot/loader/jar/Bytes.java | 2 +- .../boot/loader/jar/CentralDirectoryEndRecord.java | 2 +- .../boot/loader/jar/CentralDirectoryFileHeader.java | 0 .../boot/loader/jar/CentralDirectoryParser.java | 2 +- .../boot/loader/jar/CentralDirectoryVisitor.java | 2 +- .../springframework/boot/loader/jar/FileHeader.java | 2 +- .../springframework/boot/loader/jar/Handler.java | 2 +- .../springframework/boot/loader/jar/JarEntry.java | 2 +- .../boot/loader/jar/JarEntryCertification.java | 2 +- .../boot/loader/jar/JarEntryFilter.java | 2 +- .../springframework/boot/loader/jar/JarFile.java | 2 +- .../boot/loader/jar/JarFileEntries.java | 0 .../boot/loader/jar/JarFileWrapper.java | 2 +- .../boot/loader/jar/JarURLConnection.java | 2 +- .../boot/loader/jar/StringSequence.java | 2 +- .../boot/loader/jar/ZipInflaterInputStream.java | 2 +- .../boot/loader/jar/package-info.java | 2 +- .../boot/loader/jarmode/JarMode.java | 2 +- .../boot/loader/jarmode/JarModeLauncher.java | 2 +- .../boot/loader/jarmode/TestJarMode.java | 2 +- .../boot/loader/jarmode/package-info.java | 2 +- .../boot/loader/launch/JarLauncher.java | 0 .../boot/loader/launch/PropertiesLauncher.java | 0 .../boot/loader/launch/WarLauncher.java | 0 .../boot/loader/launch/package-info.java | 0 .../springframework/boot/loader/package-info.java | 2 +- .../boot/loader/util/SystemPropertyUtils.java | 2 +- .../boot/loader/util/package-info.java | 2 +- .../AbstractExecutableArchiveLauncherTests.java | 2 +- .../boot/loader/ClassPathIndexFileTests.java | 0 .../boot/loader/JarLauncherTests.java | 2 +- .../boot/loader/LaunchedURLClassLoaderTests.java | 0 .../boot/loader/PropertiesLauncherTests.java | 0 .../springframework/boot/loader/TestJarCreator.java | 2 +- .../boot/loader/WarLauncherTests.java | 2 +- .../boot/loader/archive/ExplodedArchiveTests.java | 0 .../boot/loader/archive/JarFileArchiveTests.java | 0 .../boot/loader/data/RandomAccessDataFileTests.java | 0 .../boot/loader/jar/AsciiBytesTests.java | 0 .../loader/jar/CentralDirectoryParserTests.java | 0 .../boot/loader/jar/HandlerTests.java | 0 .../boot/loader/jar/JarFileTests.java | 0 .../boot/loader/jar/JarFileWrapperTests.java | 0 .../boot/loader/jar/JarURLConnectionTests.java | 0 .../boot/loader/jar/JarUrlProtocolHandler.java | 0 .../boot/loader/jar/StringSequenceTests.java | 0 .../boot/loader/jarmode/LauncherJarModeTests.java | 2 +- .../boot/loader/util/SystemPropertyUtilsTests.java | 2 +- .../BOOT-INF/classes/application.properties | 0 .../test/resources/BOOT-INF/classes/bar.properties | 0 .../test/resources/BOOT-INF/classes/foo.properties | 0 .../resources/BOOT-INF/classes/loader.properties | 0 .../src/test/resources/META-INF/spring.factories | 0 .../src/test/resources/bar.properties | 0 .../test/resources/explodedsample/ExampleClass.txt | 0 .../src/test/resources/home/loader.properties | 0 .../src/test/resources/jars/app.jar | Bin .../src/test/resources/more-jars/app.jar | Bin .../src/test/resources/nested-jars/app.jar | Bin .../test/resources/nested-jars/nested-jar-app.jar | Bin .../boot/loader/classpath-index-file.idx | 0 .../resources/placeholders/META-INF/MANIFEST.MF | 0 .../test/resources/placeholders/loader.properties | 0 .../src/test/resources/root/META-INF/MANIFEST.MF | 0 .../resources/root/META-INF/spring/application.xml | 0 .../spring-boot-loader-tools/build.gradle | 8 ++++---- .../boot/loader/tools/AbstractJarWriter.java | 2 +- .../build.gradle | 8 ++++---- .../build.gradle | 0 .../settings.gradle | 0 .../boot/loaderapp/LoaderTestApplication.java | 0 .../boot/loader/LoaderIntegrationTests.java | 2 +- .../intTest/resources/conf/oracle-jdk-17/Dockerfile | 0 .../resources/conf/oracle-jdk-17/Dockerfile-aarch64 | 0 .../resources/conf/oracle-jdk-17/README.adoc | 0 .../src/intTest/resources/logback.xml | 0 .../spring-boot-smoke-test-ant/build.gradle | 2 +- .../spring-boot-smoke-test-ant/build.xml | 2 +- .../spring-boot-smoke-test-ant/ivy.xml | 2 +- 105 files changed, 67 insertions(+), 67 deletions(-) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/build.gradle (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/JarLauncher.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/Launcher.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/MainMethodRunner.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/WarLauncher.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/Archive.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/Bytes.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/FileHeader.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/Handler.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntry.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFile.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/StringSequence.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/package-info.java (92%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/package-info.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/package-info.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/util/package-info.java (92%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/JarLauncherTests.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/TestJarCreator.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/WarLauncherTests.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/application.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/bar.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/foo.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/META-INF/spring.factories (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/bar.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/explodedsample/ExampleClass.txt (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/home/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/more-jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/nested-jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/nested-jars/nested-jar-app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/placeholders/META-INF/MANIFEST.MF (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/placeholders/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/root/META-INF/MANIFEST.MF (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/root/META-INF/spring/application.xml (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/build.gradle (82%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/build.gradle (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/settings.gradle (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java (99%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/Dockerfile (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/README.adoc (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/logback.xml (100%) diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index 90ea0c29a19..0e060372d33 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -136,7 +136,7 @@ name="spring-boot-tools"> + pattern="spring-boot-(tools|antlib|configuration-.*|loader|loader-classic|.*-tools|.*-layertools|.*-plugin|autoconfigure-processor|buildpack.*)"/> diff --git a/settings.gradle b/settings.gradle index 48b547ef1c4..d5d771e915d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,7 +58,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-process include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support" include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools" -include "spring-boot-project:spring-boot-tools:spring-boot-loader" +include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic" include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools" include "spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-properties-migrator" @@ -75,7 +75,7 @@ include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" -include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" include "spring-boot-system-tests:spring-boot-deployment-tests" include "spring-boot-system-tests:spring-boot-image-tests" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e07a36ed83b..c1ffd7a6615 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1380,7 +1380,7 @@ bom { "spring-boot-devtools", "spring-boot-docker-compose", "spring-boot-jarmode-layertools", - "spring-boot-loader", + "spring-boot-loader-classic", "spring-boot-loader-tools", "spring-boot-properties-migrator", "spring-boot-starter", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle index e00c34aeaf8..750604b0194 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle @@ -19,7 +19,7 @@ dependencies { antUnit "org.apache.ant:ant-antunit:1.3" antIvy "org.apache.ivy:ivy:2.5.0" - compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) compileOnly("org.apache.ant:ant:${antVersion}") implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml index 3a0d4902d9a..980049c0cd2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml @@ -42,7 +42,7 @@ Extracting spring-boot-loader to ${destdir}/dependency - @@ -58,7 +58,7 @@ - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle index db81bc3e806..bf42c681621 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle @@ -35,7 +35,7 @@ dependencies { intTestImplementation("org.junit.jupiter:junit-jupiter") intTestImplementation("org.springframework:spring-core") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) testImplementation(project(":spring-boot-project:spring-boot")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index dd4d50894ff..7606a3d66a2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -54,7 +54,7 @@ class LoaderZipEntries { WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { + getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader-classic.jar"))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle index 1f78242394e..96d50392499 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle @@ -7,7 +7,7 @@ plugins { description = "Spring Boot Layers Tools" dependencies { - implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) implementation("org.springframework:spring-core") testImplementation("org.assertj:assertj-core") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle index 845fde0b610..17d2a7b519a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle @@ -4,7 +4,7 @@ plugins { id "org.springframework.boot.deployed" } -description = "Spring Boot Loader" +description = "Spring Boot Classic Loader" dependencies { compileOnly("org.springframework:spring-core") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java index c08407941b3..5ad01e50712 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java index 91b84b1140d..d2ceaf61c56 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java index 2c86b3d41f4..5061573e246 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index 8c7cf98ae13..7e3e2fa2239 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java index f83f685d24f..2f4cac94440 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java index 9b7a551a8b6..12355a2bef4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java index 81e0a744144..482832c1f72 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java index a99c1c2c229..c1f2bbb2f75 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java index 08734078520..f8cd52dc16f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index b30c8bb37a5..91e7bc53a48 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java index ebaca84bb95..27ce99b006f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java index ec1aa5e4a1e..e96d5ea81a0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java index 8f456bd685d..34bf2ead437 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java index 88726e37375..6a98ef68218 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java index 4b6e2678b3e..cfe121b6899 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java index 7f53bac6297..d46a22555dc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index b971b590abd..61db0b73f42 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java index 71a76785356..eff96a56e2c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java index d160cbf8477..22e04b329c3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java index 4b8de5008cf..7e4134fe564 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java index 67bf8048f04..932dea65486 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 5b8f3bedb20..8f54dc3070d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java index cbf66412e21..ffd629e0942 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java index 98ed4b905e5..6804f0ba37f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 502c450fa73..6e548048dbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java index 0a3bf030a5e..b65358947ad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index c9286b3e8b5..859ae88ab00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java index 9e6af077ed9..12850a4ebe3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java index 87587bed3ff..67624460ccd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java index e232261ff47..638afe45f49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java index c711e206f5d..162e4a6a739 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java index 44248893410..600266a241b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java index 6a6e83ff23c..2e17175690a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java index 315cb5696b8..2f3b5a74e8f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/package-info.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/package-info.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java index c2114c2d83b..4b32f644f54 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java index b6f0e3a3a7f..df00705e9ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java index af0aa2d1a7d..d3d7eef2d9d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java index 48d7340ee38..60e3cb2765e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java index fa713034304..afa32a7c4f1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 100e2c757e3..c5c5fd3b95c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java index aef78cfa53d..fbab8d36ed0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java index 93179bad6fe..dec587e18bb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java index 0697b77b7bb..802a762e79d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/application.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/application.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/bar.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/bar.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/foo.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/foo.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/META-INF/spring.factories similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/META-INF/spring.factories diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/bar.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/bar.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/explodedsample/ExampleClass.txt similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/explodedsample/ExampleClass.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/home/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/home/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/more-jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/more-jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/nested-jar-app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/nested-jar-app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/META-INF/MANIFEST.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/META-INF/MANIFEST.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/MANIFEST.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/MANIFEST.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/spring/application.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/spring/application.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 755f1cc7bc7..80311be0acb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -36,7 +36,7 @@ dependencies { compileOnly("ch.qos.logback:logback-classic") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) @@ -57,7 +57,7 @@ task reproducibleLoaderJar(type: Jar) { } reproducibleFileOrder = true preserveFileTimestamps = false - archiveFileName = "spring-boot-loader.jar" + archiveFileName = "spring-boot-loader-classic.jar" destinationDirectory = file("${generatedResources}/META-INF/loader") } @@ -78,6 +78,6 @@ sourceSets { compileJava { if ((!project.hasProperty("toolchainVersion")) && JavaVersion.current() == JavaVersion.VERSION_1_8) { - options.compilerArgs += ['-Xlint:-sunapi', '-XDenableSunApiLintControl'] - } + options.compilerArgs += ['-Xlint:-sunapi', '-XDenableSunApiLintControl'] + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index ffbdf5ec73b..23282dbea72 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -51,7 +51,7 @@ import org.apache.commons.compress.archivers.zip.UnixStat; */ public abstract class AbstractJarWriter implements LoaderClassesWriter { - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; + private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader-classic.jar"; private static final int BUFFER_SIZE = 32 * 1024; diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle similarity index 82% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle index 7c4095f73b1..d05a3d6c9e0 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle @@ -4,7 +4,7 @@ plugins { id "org.springframework.boot.integration-test" } -description = "Spring Boot Loader Integration Tests" +description = "Spring Boot Classic Loader Integration Tests" configurations { app @@ -28,13 +28,13 @@ task syncMavenRepository(type: Sync) { } task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { - sourceDirectory = file("spring-boot-loader-tests-app") - destinationDirectory = file("${buildDir}/spring-boot-loader-tests-app") + sourceDirectory = file("spring-boot-loader-classic-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-loader-classic-tests-app") } task buildApp(type: GradleBuild) { dependsOn syncAppSource, syncMavenRepository - dir = "${buildDir}/spring-boot-loader-tests-app" + dir = "${buildDir}/spring-boot-loader-classic-tests-app" startParameter.buildCacheEnabled = false tasks = ["build"] } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/settings.gradle similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/settings.gradle diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java similarity index 99% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 9acac3f61da..b11478b61c7 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -71,7 +71,7 @@ class LoaderIntegrationTests { } private File findApplication() { - String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app"); + String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-classic-tests-app"); File jar = new File(name); Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?"); return jar; diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/logback.xml similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/logback.xml diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle index 7ff1740185f..7f90497a4ff 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle @@ -30,7 +30,7 @@ dependencies { antDependencies "org.apache.ant:ant-launcher:1.10.7" antDependencies "org.apache.ant:ant:1.10.7" - testRepository(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader", configuration: "mavenRepository")) + testRepository(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader-classic", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository")) testImplementation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml index 091e4aa1167..a03067231ce 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml @@ -65,7 +65,7 @@ - + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml index 192d5281fcd..2ecb5cc31a2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml @@ -7,6 +7,6 @@ - + From a89057b7c7ccece78b16152dfc80b75b17476b35 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 13:53:58 -0700 Subject: [PATCH 2/3] Reintroduce spring-boot-loader modules Restore the `spring-boot-loader` with the previous loader code so that we can develop it further. See gh-37669 --- settings.gradle | 2 + .../spring-boot-dependencies/build.gradle | 1 + .../spring-boot-loader/build.gradle | 23 + .../boot/loader/ClassPathIndexFile.java | 123 +++ .../loader/ExecutableArchiveLauncher.java | 207 +++++ .../boot/loader/JarLauncher.java | 68 ++ .../boot/loader/LaunchedURLClassLoader.java | 366 +++++++++ .../springframework/boot/loader/Launcher.java | 159 ++++ .../boot/loader/MainMethodRunner.java | 52 ++ .../boot/loader/PropertiesLauncher.java | 726 +++++++++++++++++ .../boot/loader/WarLauncher.java | 62 ++ .../boot/loader/archive/Archive.java | 115 +++ .../boot/loader/archive/ExplodedArchive.java | 342 ++++++++ .../boot/loader/archive/JarFileArchive.java | 310 ++++++++ .../boot/loader/archive/package-info.java | 23 + .../boot/loader/data/RandomAccessData.java | 74 ++ .../loader/data/RandomAccessDataFile.java | 262 +++++++ .../boot/loader/data/package-info.java | 22 + .../boot/loader/jar/AbstractJarFile.java | 78 ++ .../boot/loader/jar/AsciiBytes.java | 255 ++++++ .../boot/loader/jar/Bytes.java | 37 + .../loader/jar/CentralDirectoryEndRecord.java | 258 ++++++ .../jar/CentralDirectoryFileHeader.java | 222 ++++++ .../loader/jar/CentralDirectoryParser.java | 101 +++ .../loader/jar/CentralDirectoryVisitor.java | 34 + .../boot/loader/jar/FileHeader.java | 64 ++ .../boot/loader/jar/Handler.java | 466 +++++++++++ .../boot/loader/jar/JarEntry.java | 120 +++ .../loader/jar/JarEntryCertification.java | 58 ++ .../boot/loader/jar/JarEntryFilter.java | 35 + .../boot/loader/jar/JarFile.java | 475 +++++++++++ .../boot/loader/jar/JarFileEntries.java | 491 ++++++++++++ .../boot/loader/jar/JarFileWrapper.java | 126 +++ .../boot/loader/jar/JarURLConnection.java | 393 ++++++++++ .../boot/loader/jar/StringSequence.java | 157 ++++ .../loader/jar/ZipInflaterInputStream.java | 88 +++ .../boot/loader/jar/package-info.java | 20 + .../boot/loader/jarmode/JarMode.java | 42 + .../boot/loader/jarmode/JarModeLauncher.java | 53 ++ .../boot/loader/jarmode/TestJarMode.java | 38 + .../boot/loader/jarmode/package-info.java | 22 + .../boot/loader/launch/JarLauncher.java | 34 + .../loader/launch/PropertiesLauncher.java | 34 + .../boot/loader/launch/WarLauncher.java | 34 + .../boot/loader/launch/package-info.java | 23 + .../boot/loader/package-info.java | 26 + .../boot/loader/util/SystemPropertyUtils.java | 232 ++++++ .../boot/loader/util/package-info.java | 20 + ...bstractExecutableArchiveLauncherTests.java | 149 ++++ .../boot/loader/ClassPathIndexFileTests.java | 109 +++ .../boot/loader/JarLauncherTests.java | 154 ++++ .../loader/LaunchedURLClassLoaderTests.java | 111 +++ .../boot/loader/PropertiesLauncherTests.java | 433 +++++++++++ .../boot/loader/TestJarCreator.java | 151 ++++ .../boot/loader/WarLauncherTests.java | 121 +++ .../loader/archive/ExplodedArchiveTests.java | 189 +++++ .../loader/archive/JarFileArchiveTests.java | 207 +++++ .../data/RandomAccessDataFileTests.java | 300 +++++++ .../boot/loader/jar/AsciiBytesTests.java | 196 +++++ .../jar/CentralDirectoryParserTests.java | 139 ++++ .../boot/loader/jar/HandlerTests.java | 210 +++++ .../boot/loader/jar/JarFileTests.java | 736 ++++++++++++++++++ .../boot/loader/jar/JarFileWrapperTests.java | 281 +++++++ .../loader/jar/JarURLConnectionTests.java | 246 ++++++ .../loader/jar/JarUrlProtocolHandler.java | 57 ++ .../boot/loader/jar/StringSequenceTests.java | 220 ++++++ .../loader/jarmode/LauncherJarModeTests.java | 86 ++ .../loader/util/SystemPropertyUtilsTests.java | 62 ++ .../BOOT-INF/classes/application.properties | 1 + .../resources/BOOT-INF/classes/bar.properties | 1 + .../resources/BOOT-INF/classes/foo.properties | 3 + .../BOOT-INF/classes/loader.properties | 1 + .../test/resources/META-INF/spring.factories | 3 + .../src/test/resources/bar.properties | 1 + .../resources/explodedsample/ExampleClass.txt | 26 + .../src/test/resources/home/loader.properties | 1 + .../src/test/resources/jars/app.jar | Bin 0 -> 2213 bytes .../src/test/resources/more-jars/app.jar | Bin 0 -> 1150 bytes .../src/test/resources/nested-jars/app.jar | Bin 0 -> 3313 bytes .../resources/nested-jars/nested-jar-app.jar | Bin 0 -> 1408 bytes .../boot/loader/classpath-index-file.idx | 5 + .../placeholders/META-INF/MANIFEST.MF | 2 + .../resources/placeholders/loader.properties | 1 + .../test/resources/root/META-INF/MANIFEST.MF | 1 + .../root/META-INF/spring/application.xml | 6 + .../spring-boot-loader-tests/build.gradle | 44 ++ .../spring-boot-loader-tests-app/build.gradle | 18 + .../settings.gradle | 15 + .../boot/loaderapp/LoaderTestApplication.java | 59 ++ .../boot/loader/LoaderIntegrationTests.java | 139 ++++ .../resources/conf/oracle-jdk-17/Dockerfile | 8 + .../conf/oracle-jdk-17/Dockerfile-aarch64 | 8 + .../resources/conf/oracle-jdk-17/README.adoc | 5 + .../src/intTest/resources/logback.xml | 4 + 94 files changed, 11482 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index d5d771e915d..9dc4ab6ee40 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,6 +58,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-process include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support" include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools" +include "spring-boot-project:spring-boot-tools:spring-boot-loader" include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic" include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools" include "spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" @@ -75,6 +76,7 @@ include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" include "spring-boot-system-tests:spring-boot-deployment-tests" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c1ffd7a6615..3f7bac1a79b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1380,6 +1380,7 @@ bom { "spring-boot-devtools", "spring-boot-docker-compose", "spring-boot-jarmode-layertools", + "spring-boot-loader", "spring-boot-loader-classic", "spring-boot-loader-tools", "spring-boot-properties-migrator", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle new file mode 100644 index 00000000000..845fde0b610 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Loader" + +dependencies { + compileOnly("org.springframework:spring-core") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.springframework:spring-test") + testImplementation("org.springframework:spring-core-test") + + testRuntimeOnly("ch.qos.logback:logback-classic") + testRuntimeOnly("org.bouncycastle:bcprov-jdk18on:1.71") + testRuntimeOnly("org.springframework:spring-webmvc") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java new file mode 100644 index 00000000000..5ad01e50712 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A class path index file that provides ordering information for JARs. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +final class ClassPathIndexFile { + + private final File root; + + private final List lines; + + private ClassPathIndexFile(File root, List lines) { + this.root = root; + this.lines = lines.stream().map(this::extractName).toList(); + } + + private String extractName(String line) { + if (line.startsWith("- \"") && line.endsWith("\"")) { + return line.substring(3, line.length() - 1); + } + throw new IllegalStateException("Malformed classpath index line [" + line + "]"); + } + + int size() { + return this.lines.size(); + } + + boolean containsEntry(String name) { + if (name == null || name.isEmpty()) { + return false; + } + return this.lines.contains(name); + } + + List getUrls() { + return this.lines.stream().map(this::asUrl).toList(); + } + + private URL asUrl(String line) { + try { + return new File(this.root, line).toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException { + return loadIfPossible(asFile(root), location); + } + + private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { + return loadIfPossible(root, new File(root, location)); + } + + private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException { + if (indexFile.exists() && indexFile.isFile()) { + try (InputStream inputStream = new FileInputStream(indexFile)) { + return new ClassPathIndexFile(root, loadLines(inputStream)); + } + } + return null; + } + + private static List loadLines(InputStream inputStream) throws IOException { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = reader.readLine(); + while (line != null) { + if (!line.trim().isEmpty()) { + lines.add(line); + } + line = reader.readLine(); + } + return Collections.unmodifiableList(lines); + } + + private static File asFile(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException("URL does not reference a file"); + } + try { + return new File(url.toURI()); + } + catch (URISyntaxException ex) { + return new File(url.getPath()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java new file mode 100644 index 00000000000..d2ceaf61c56 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; + +/** + * Base class for executable archive {@link Launcher}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @since 1.0.0 + */ +public abstract class ExecutableArchiveLauncher extends Launcher { + + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + + protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + + protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx"; + + private final Archive archive; + + private final ClassPathIndexFile classPathIndex; + + public ExecutableArchiveLauncher() { + try { + this.archive = createArchive(); + this.classPathIndex = getClassPathIndex(this.archive); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected ExecutableArchiveLauncher(Archive archive) { + try { + this.archive = archive; + this.classPathIndex = getClassPathIndex(this.archive); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + // Only needed for exploded archives, regular ones already have a defined order + if (archive instanceof ExplodedArchive) { + String location = getClassPathIndexFileLocation(archive); + return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); + } + return null; + } + + private String getClassPathIndexFileLocation(Archive archive) throws IOException { + Manifest manifest = archive.getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; + return (location != null) ? location : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME; + } + + @Override + protected String getMainClass() throws Exception { + Manifest manifest = this.archive.getManifest(); + String mainClass = null; + if (manifest != null) { + mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); + } + if (mainClass == null) { + throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); + } + return mainClass; + } + + @Override + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(guessClassPathSize()); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + if (this.classPathIndex != null) { + urls.addAll(this.classPathIndex.getUrls()); + } + return createClassLoader(urls.toArray(new URL[0])); + } + + private int guessClassPathSize() { + if (this.classPathIndex != null) { + return this.classPathIndex.size() + 10; + } + return 50; + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + Archive.EntryFilter searchFilter = this::isSearchCandidate; + Iterator archives = this.archive.getNestedArchives(searchFilter, + (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)); + if (isPostProcessingClassPathArchives()) { + archives = applyClassPathArchivePostProcessing(archives); + } + return archives; + } + + private boolean isEntryIndexed(Archive.Entry entry) { + if (this.classPathIndex != null) { + return this.classPathIndex.containsEntry(entry.getName()); + } + return false; + } + + private Iterator applyClassPathArchivePostProcessing(Iterator archives) throws Exception { + List list = new ArrayList<>(); + while (archives.hasNext()) { + list.add(archives.next()); + } + postProcessClassPathArchives(list); + return list.iterator(); + } + + /** + * Determine if the specified entry is a candidate for further searching. + * @param entry the entry to check + * @return {@code true} if the entry is a candidate for further searching + * @since 2.3.0 + */ + protected boolean isSearchCandidate(Archive.Entry entry) { + if (getArchiveEntryPathPrefix() == null) { + return true; + } + return entry.getName().startsWith(getArchiveEntryPathPrefix()); + } + + /** + * Determine if the specified entry is a nested item that should be added to the + * classpath. + * @param entry the entry to check + * @return {@code true} if the entry is a nested item (jar or directory) + */ + protected abstract boolean isNestedArchive(Archive.Entry entry); + + /** + * Return if post-processing needs to be applied to the archives. For back + * compatibility this method returns {@code true}, but subclasses that don't override + * {@link #postProcessClassPathArchives(List)} should provide an implementation that + * returns {@code false}. + * @return if the {@link #postProcessClassPathArchives(List)} method is implemented + * @since 2.3.0 + */ + protected boolean isPostProcessingClassPathArchives() { + return true; + } + + /** + * Called to post-process archive entries before they are used. Implementations can + * add and remove entries. + * @param archives the archives + * @throws Exception if the post-processing fails + * @see #isPostProcessingClassPathArchives() + */ + protected void postProcessClassPathArchives(List archives) throws Exception { + } + + /** + * Return the path prefix for entries in the archive. + * @return the path prefix + */ + protected String getArchiveEntryPathPrefix() { + return null; + } + + @Override + protected boolean isExploded() { + return this.archive.isExploded(); + } + + @Override + protected final Archive getArchive() { + return this.archive; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java new file mode 100644 index 00000000000..5061573e246 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.EntryFilter; + +/** + * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are + * included inside a {@code /BOOT-INF/lib} directory and that application classes are + * included inside a {@code /BOOT-INF/classes} directory. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @since 1.0.0 + */ +public class JarLauncher extends ExecutableArchiveLauncher { + + static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> { + if (entry.isDirectory()) { + return entry.getName().equals("BOOT-INF/classes/"); + } + return entry.getName().startsWith("BOOT-INF/lib/"); + }; + + public JarLauncher() { + } + + protected JarLauncher(Archive archive) { + super(archive); + } + + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + protected boolean isNestedArchive(Archive.Entry entry) { + return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry); + } + + @Override + protected String getArchiveEntryPathPrefix() { + return "BOOT-INF/"; + } + + public static void main(String[] args) throws Exception { + new JarLauncher().launch(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java new file mode 100644 index 00000000000..7e3e2fa2239 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -0,0 +1,366 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.jar.Handler; + +/** + * {@link ClassLoader} used by the {@link Launcher}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class LaunchedURLClassLoader extends URLClassLoader { + + private static final int BUFFER_SIZE = 4096; + + static { + ClassLoader.registerAsParallelCapable(); + } + + private final boolean exploded; + + private final Archive rootArchive; + + private final Object packageLock = new Object(); + + private volatile DefinePackageCallType definePackageCallType; + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { + this(false, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) { + this(exploded, null, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param rootArchive the root archive or {@code null} + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + * @since 2.3.1 + */ + public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { + super(urls, parent); + this.exploded = exploded; + this.rootArchive = rootArchive; + } + + @Override + public URL findResource(String name) { + if (this.exploded) { + return super.findResource(name); + } + Handler.setUseFastConnectionExceptions(true); + try { + return super.findResource(name); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + @Override + public Enumeration findResources(String name) throws IOException { + if (this.exploded) { + return super.findResources(name); + } + Handler.setUseFastConnectionExceptions(true); + try { + return new UseFastConnectionExceptionsEnumeration(super.findResources(name)); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.springframework.boot.loader.jarmode.")) { + try { + Class result = loadClassInLaunchedClassLoader(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + } + } + if (this.exploded) { + return super.loadClass(name, resolve); + } + Handler.setUseFastConnectionExceptions(true); + try { + try { + definePackageIfNecessary(name); + } + catch (IllegalArgumentException ex) { + // Tolerate race condition due to being parallel capable + if (getDefinedPackage(name) == null) { + // This should never happen as the IllegalArgumentException indicates + // that the package has already been defined and, therefore, + // getDefinedPackage(name) should not return null. + throw new AssertionError("Package " + name + " has already been defined but it could not be found"); + } + } + return super.loadClass(name, resolve); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + private Class loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException { + String internalName = name.replace('.', '/') + ".class"; + InputStream inputStream = getParent().getResourceAsStream(internalName); + if (inputStream == null) { + throw new ClassNotFoundException(name); + } + try { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + byte[] bytes = outputStream.toByteArray(); + Class definedClass = defineClass(name, bytes, 0, bytes.length); + definePackageIfNecessary(name); + return definedClass; + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + /** + * Define a package before a {@code findClass} call is made. This is necessary to + * ensure that the appropriate manifest for nested JARs is associated with the + * package. + * @param className the class name being found + */ + private void definePackageIfNecessary(String className) { + int lastDot = className.lastIndexOf('.'); + if (lastDot >= 0) { + String packageName = className.substring(0, lastDot); + if (getDefinedPackage(packageName) == null) { + try { + definePackage(className, packageName); + } + catch (IllegalArgumentException ex) { + // Tolerate race condition due to being parallel capable + if (getDefinedPackage(packageName) == null) { + // This should never happen as the IllegalArgumentException + // indicates that the package has already been defined and, + // therefore, getDefinedPackage(name) should not have returned + // null. + throw new AssertionError( + "Package " + packageName + " has already been defined but it could not be found"); + } + } + } + } + } + + private void definePackage(String className, String packageName) { + String packageEntryName = packageName.replace('.', '/') + "/"; + String classEntryName = className.replace('.', '/') + ".class"; + for (URL url : getURLs()) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection jarURLConnection) { + JarFile jarFile = jarURLConnection.getJarFile(); + if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null + && jarFile.getManifest() != null) { + definePackage(packageName, jarFile.getManifest(), url); + return; + } + } + } + catch (IOException ex) { + // Ignore + } + } + } + + @Override + protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, man, url); + } + synchronized (this.packageLock) { + return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url)); + } + } + + @Override + protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, + String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, + sealBase); + } + synchronized (this.packageLock) { + if (this.definePackageCallType == null) { + // We're not part of a call chain which means that the URLClassLoader + // is trying to define a package for our exploded JAR. We use the + // manifest version to ensure package attributes are set + Manifest manifest = getManifest(this.rootArchive); + if (manifest != null) { + return definePackage(name, manifest, sealBase); + } + } + return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle, + specVersion, specVendor, implTitle, implVersion, implVendor, sealBase)); + } + } + + private Manifest getManifest(Archive archive) { + try { + return (archive != null) ? archive.getManifest() : null; + } + catch (IOException ex) { + return null; + } + } + + private T doDefinePackage(DefinePackageCallType type, Supplier call) { + DefinePackageCallType existingType = this.definePackageCallType; + try { + this.definePackageCallType = type; + return call.get(); + } + finally { + this.definePackageCallType = existingType; + } + } + + /** + * Clear URL caches. + */ + public void clearCache() { + if (this.exploded) { + return; + } + for (URL url : getURLs()) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + clearCache(connection); + } + } + catch (IOException ex) { + // Ignore + } + } + + } + + private void clearCache(URLConnection connection) throws IOException { + Object jarFile = ((JarURLConnection) connection).getJarFile(); + if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) { + ((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache(); + } + } + + private static class UseFastConnectionExceptionsEnumeration implements Enumeration { + + private final Enumeration delegate; + + UseFastConnectionExceptionsEnumeration(Enumeration delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasMoreElements() { + Handler.setUseFastConnectionExceptions(true); + try { + return this.delegate.hasMoreElements(); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + + } + + @Override + public URL nextElement() { + Handler.setUseFastConnectionExceptions(true); + try { + return this.delegate.nextElement(); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + } + + /** + * The different types of call made to define a package. We track these for exploded + * jars so that we can detect packages that should have manifest attributes applied. + */ + private enum DefinePackageCallType { + + /** + * A define package call from a resource that has a manifest. + */ + MANIFEST, + + /** + * A define package call with a direct set of attributes. + */ + ATTRIBUTES + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java new file mode 100644 index 00000000000..2f4cac94440 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.jar.JarFile; + +/** + * Base class for launchers that can start an application with a fully configured + * classpath backed by one or more {@link Archive}s. + * + * @author Phillip Webb + * @author Dave Syer + * @since 1.0.0 + */ +public abstract class Launcher { + + private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher"; + + /** + * Launch the application. This method is the initial entry point that should be + * called by a subclass {@code public static void main(String[] args)} method. + * @param args the incoming arguments + * @throws Exception if the application fails to launch + */ + protected void launch(String[] args) throws Exception { + if (!isExploded()) { + JarFile.registerUrlProtocolHandler(); + } + ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); + String jarMode = System.getProperty("jarmode"); + String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); + launch(args, launchClass, classLoader); + } + + /** + * Create a classloader for the specified archives. + * @param archives the archives + * @return the classloader + * @throws Exception if the classloader cannot be created + * @since 2.3.0 + */ + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(50); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + return createClassLoader(urls.toArray(new URL[0])); + } + + /** + * Create a classloader for the specified URLs. + * @param urls the URLs + * @return the classloader + * @throws Exception if the classloader cannot be created + */ + protected ClassLoader createClassLoader(URL[] urls) throws Exception { + return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader()); + } + + /** + * Launch the application given the archive file and a fully configured classloader. + * @param args the incoming arguments + * @param launchClass the launch class to run + * @param classLoader the classloader + * @throws Exception if the launch fails + */ + protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { + Thread.currentThread().setContextClassLoader(classLoader); + createMainMethodRunner(launchClass, args, classLoader).run(); + } + + /** + * Create the {@code MainMethodRunner} used to launch the application. + * @param mainClass the main class + * @param args the incoming arguments + * @param classLoader the classloader + * @return the main method runner + */ + protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { + return new MainMethodRunner(mainClass, args); + } + + /** + * Returns the main class that should be launched. + * @return the name of the main class + * @throws Exception if the main class cannot be obtained + */ + protected abstract String getMainClass() throws Exception; + + /** + * Returns the archives that will be used to construct the class path. + * @return the class path archives + * @throws Exception if the class path archives cannot be obtained + * @since 2.3.0 + */ + protected abstract Iterator getClassPathArchivesIterator() throws Exception; + + protected final Archive createArchive() throws Exception { + ProtectionDomain protectionDomain = getClass().getProtectionDomain(); + CodeSource codeSource = protectionDomain.getCodeSource(); + URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; + String path = (location != null) ? location.getSchemeSpecificPart() : null; + if (path == null) { + throw new IllegalStateException("Unable to determine code source archive"); + } + File root = new File(path); + if (!root.exists()) { + throw new IllegalStateException("Unable to determine code source archive from " + root); + } + return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); + } + + /** + * Returns if the launcher is running in an exploded mode. If this method returns + * {@code true} then only regular JARs are supported and the additional URL and + * ClassLoader support infrastructure can be optimized. + * @return if the jar is exploded. + * @since 2.3.0 + */ + protected boolean isExploded() { + return false; + } + + /** + * Return the root archive. + * @return the root archive + * @since 2.3.1 + */ + protected Archive getArchive() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java new file mode 100644 index 00000000000..12355a2bef4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.lang.reflect.Method; + +/** + * Utility class that is used by {@link Launcher}s to call a main method. The class + * containing the main method is loaded using the thread context class loader. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class MainMethodRunner { + + private final String mainClassName; + + private final String[] args; + + /** + * Create a new {@link MainMethodRunner} instance. + * @param mainClass the main class + * @param args incoming arguments + */ + public MainMethodRunner(String mainClass, String[] args) { + this.mainClassName = mainClass; + this.args = (args != null) ? args.clone() : null; + } + + public void run() throws Exception { + Class mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); + Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); + mainMethod.invoke(null, new Object[] { this.args }); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java new file mode 100755 index 00000000000..3703ac13670 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -0,0 +1,726 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.boot.loader.archive.Archive.EntryFilter; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.util.SystemPropertyUtils; + +/** + * {@link Launcher} for archives with user-configured classpath and main class through a + * properties file. This model is often more flexible and more amenable to creating + * well-behaved OS-level services than a model based on executable jars. + *

+ * Looks in various places for a properties file to extract loader settings, defaulting to + * {@code loader.properties} either on the current classpath or in the current working + * directory. The name of the properties file can be changed by setting a System property + * {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for + * {@code foo.properties}. If that file doesn't exist then tries + * {@code loader.config.location} (with allowed prefixes {@code classpath:} and + * {@code file:} or any valid URL). Once that file is located turns it into Properties and + * extracts optional values (which can also be provided overridden as System properties in + * case the file doesn't exist): + *

    + *
  • {@code loader.path}: a comma-separated list of directories (containing file + * resources and/or nested archives in *.jar or *.zip or archives) or archives to append + * to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are + * always used
  • + *
  • {@code loader.main}: the main method to delegate execution to once the class loader + * is set up. No default, but will fall back to looking for a {@code Start-Class} in a + * {@code MANIFEST.MF}, if there is one in ${loader.home}/META-INF.
  • + *
+ * + * @author Dave Syer + * @author Janne Valkealahti + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class PropertiesLauncher extends Launcher { + + private static final Class[] PARENT_ONLY_PARAMS = new Class[] { ClassLoader.class }; + + private static final Class[] URLS_AND_PARENT_PARAMS = new Class[] { URL[].class, ClassLoader.class }; + + private static final Class[] NO_PARAMS = new Class[] {}; + + private static final URL[] NO_URLS = new URL[0]; + + private static final String DEBUG = "loader.debug"; + + /** + * Properties key for main class. As a manifest entry can also be specified as + * {@code Start-Class}. + */ + public static final String MAIN = "loader.main"; + + /** + * Properties key for classpath entries (directories possibly containing jars or + * jars). Multiple entries can be specified using a comma-separated list. {@code + * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used. + */ + public static final String PATH = "loader.path"; + + /** + * Properties key for home directory. This is the location of external configuration + * if not on classpath, and also the base path for any relative paths in the + * {@link #PATH loader path}. Defaults to current working directory ( + * ${user.dir}). + */ + public static final String HOME = "loader.home"; + + /** + * Properties key for default command line arguments. These arguments (if present) are + * prepended to the main method arguments before launching. + */ + public static final String ARGS = "loader.args"; + + /** + * Properties key for name of external configuration file (excluding suffix). Defaults + * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is + * provided instead. + */ + public static final String CONFIG_NAME = "loader.config.name"; + + /** + * Properties key for config file location (including optional classpath:, file: or + * URL prefix). + */ + public static final String CONFIG_LOCATION = "loader.config.location"; + + /** + * Properties key for boolean flag (default false) which, if set, will cause the + * external configuration properties to be copied to System properties (assuming that + * is allowed by Java security). + */ + public static final String SET_SYSTEM_PROPERTIES = "loader.system"; + + private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); + + private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator; + + private final File home; + + private List paths = new ArrayList<>(); + + private final Properties properties = new Properties(); + + private final Archive parent; + + private volatile ClassPathArchives classPathArchives; + + public PropertiesLauncher() { + try { + this.home = getHomeDirectory(); + initializeProperties(); + initializePaths(); + this.parent = createArchive(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected File getHomeDirectory() { + try { + return new File(getPropertyWithDefault(HOME, "${user.dir}")); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private void initializeProperties() throws Exception { + List configs = new ArrayList<>(); + if (getProperty(CONFIG_LOCATION) != null) { + configs.add(getProperty(CONFIG_LOCATION)); + } + else { + String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(","); + for (String name : names) { + configs.add("file:" + getHomeDirectory() + "/" + name + ".properties"); + configs.add("classpath:" + name + ".properties"); + configs.add("classpath:BOOT-INF/classes/" + name + ".properties"); + } + } + for (String config : configs) { + try (InputStream resource = getResource(config)) { + if (resource != null) { + debug("Found: " + config); + loadResource(resource); + // Load the first one we find + return; + } + else { + debug("Not found: " + config); + } + } + } + } + + private void loadResource(InputStream resource) throws Exception { + this.properties.load(resource); + for (Object key : Collections.list(this.properties.propertyNames())) { + String text = this.properties.getProperty((String) key); + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text); + if (value != null) { + this.properties.put(key, value); + } + } + if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) { + debug("Adding resolved properties to System properties"); + for (Object key : Collections.list(this.properties.propertyNames())) { + String value = this.properties.getProperty((String) key); + System.setProperty((String) key, value); + } + } + } + + private InputStream getResource(String config) throws Exception { + if (config.startsWith("classpath:")) { + return getClasspathResource(config.substring("classpath:".length())); + } + config = handleUrl(config); + if (isUrl(config)) { + return getURLResource(config); + } + return getFileResource(config); + } + + private String handleUrl(String path) throws UnsupportedEncodingException { + if (path.startsWith("jar:file:") || path.startsWith("file:")) { + path = URLDecoder.decode(path, "UTF-8"); + if (path.startsWith("file:")) { + path = path.substring("file:".length()); + if (path.startsWith("//")) { + path = path.substring(2); + } + } + } + return path; + } + + private boolean isUrl(String config) { + return config.contains("://"); + } + + private InputStream getClasspathResource(String config) { + while (config.startsWith("/")) { + config = config.substring(1); + } + config = "/" + config; + debug("Trying classpath: " + config); + return getClass().getResourceAsStream(config); + } + + private InputStream getFileResource(String config) throws Exception { + File file = new File(config); + debug("Trying file: " + config); + if (file.canRead()) { + return new FileInputStream(file); + } + return null; + } + + private InputStream getURLResource(String config) throws Exception { + URL url = new URL(config); + if (exists(url)) { + URLConnection con = url.openConnection(); + try { + return con.getInputStream(); + } + catch (IOException ex) { + // Close the HTTP connection (if applicable). + if (con instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); + } + throw ex; + } + } + return null; + } + + private boolean exists(URL url) throws IOException { + // Try a URL connection content-length header... + URLConnection connection = url.openConnection(); + try { + connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP")); + if (connection instanceof HttpURLConnection httpConnection) { + httpConnection.setRequestMethod("HEAD"); + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return true; + } + else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + } + return (connection.getContentLength() >= 0); + } + finally { + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); + } + } + } + + private void initializePaths() throws Exception { + String path = getProperty(PATH); + if (path != null) { + this.paths = parsePathsProperty(path); + } + debug("Nested archive paths: " + this.paths); + } + + private List parsePathsProperty(String commaSeparatedPaths) { + List paths = new ArrayList<>(); + for (String path : commaSeparatedPaths.split(",")) { + path = cleanupPath(path); + // "" means the user wants root of archive but not current directory + path = (path == null || path.isEmpty()) ? "/" : path; + paths.add(path); + } + if (paths.isEmpty()) { + paths.add("lib"); + } + return paths; + } + + protected String[] getArgs(String... args) throws Exception { + String loaderArgs = getProperty(ARGS); + if (loaderArgs != null) { + String[] defaultArgs = loaderArgs.split("\\s+"); + String[] additionalArgs = args; + args = new String[defaultArgs.length + additionalArgs.length]; + System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); + System.arraycopy(additionalArgs, 0, args, defaultArgs.length, additionalArgs.length); + } + return args; + } + + @Override + protected String getMainClass() throws Exception { + String mainClass = getProperty(MAIN, "Start-Class"); + if (mainClass == null) { + throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified"); + } + return mainClass; + } + + @Override + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + String customLoaderClassName = getProperty("loader.classLoader"); + if (customLoaderClassName == null) { + return super.createClassLoader(archives); + } + Set urls = new LinkedHashSet<>(); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader()); + debug("Classpath for custom loader: " + urls); + loader = wrapWithCustomClassLoader(loader, customLoaderClassName); + debug("Using custom class loader: " + customLoaderClassName); + return loader; + } + + @SuppressWarnings("unchecked") + private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String className) throws Exception { + Class type = (Class) Class.forName(className, true, parent); + ClassLoader classLoader = newClassLoader(type, PARENT_ONLY_PARAMS, parent); + if (classLoader == null) { + classLoader = newClassLoader(type, URLS_AND_PARENT_PARAMS, NO_URLS, parent); + } + if (classLoader == null) { + classLoader = newClassLoader(type, NO_PARAMS); + } + if (classLoader == null) { + throw new IllegalArgumentException("Unable to create class loader for " + className); + } + return classLoader; + } + + private ClassLoader newClassLoader(Class loaderClass, Class[] parameterTypes, Object... initargs) + throws Exception { + try { + Constructor constructor = loaderClass.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + return constructor.newInstance(initargs); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + private String getProperty(String propertyKey) throws Exception { + return getProperty(propertyKey, null, null); + } + + private String getProperty(String propertyKey, String manifestKey) throws Exception { + return getProperty(propertyKey, manifestKey, null); + } + + private String getPropertyWithDefault(String propertyKey, String defaultValue) throws Exception { + return getProperty(propertyKey, null, defaultValue); + } + + private String getProperty(String propertyKey, String manifestKey, String defaultValue) throws Exception { + if (manifestKey == null) { + manifestKey = propertyKey.replace('.', '-'); + manifestKey = toCamelCase(manifestKey); + } + String property = SystemPropertyUtils.getProperty(propertyKey); + if (property != null) { + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property); + debug("Property '" + propertyKey + "' from environment: " + value); + return value; + } + if (this.properties.containsKey(propertyKey)) { + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, + this.properties.getProperty(propertyKey)); + debug("Property '" + propertyKey + "' from properties: " + value); + return value; + } + try { + if (this.home != null) { + // Prefer home dir for MANIFEST if there is one + try (ExplodedArchive archive = new ExplodedArchive(this.home, false)) { + Manifest manifest = archive.getManifest(); + if (manifest != null) { + String value = manifest.getMainAttributes().getValue(manifestKey); + if (value != null) { + debug("Property '" + manifestKey + "' from home directory manifest: " + value); + return SystemPropertyUtils.resolvePlaceholders(this.properties, value); + } + } + } + } + } + catch (IllegalStateException ex) { + // Ignore + } + // Otherwise try the parent archive + Manifest manifest = createArchive().getManifest(); + if (manifest != null) { + String value = manifest.getMainAttributes().getValue(manifestKey); + if (value != null) { + debug("Property '" + manifestKey + "' from archive manifest: " + value); + return SystemPropertyUtils.resolvePlaceholders(this.properties, value); + } + } + return (defaultValue != null) ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue) + : defaultValue; + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + ClassPathArchives classPathArchives = this.classPathArchives; + if (classPathArchives == null) { + classPathArchives = new ClassPathArchives(); + this.classPathArchives = classPathArchives; + } + return classPathArchives.iterator(); + } + + public static void main(String[] args) throws Exception { + PropertiesLauncher launcher = new PropertiesLauncher(); + args = launcher.getArgs(args); + launcher.launch(args); + } + + public static String toCamelCase(CharSequence string) { + if (string == null) { + return null; + } + StringBuilder builder = new StringBuilder(); + Matcher matcher = WORD_SEPARATOR.matcher(string); + int pos = 0; + while (matcher.find()) { + builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); + pos = matcher.end(); + } + builder.append(capitalize(string.subSequence(pos, string.length()).toString())); + return builder.toString(); + } + + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + private void debug(String message) { + if (Boolean.getBoolean(DEBUG)) { + System.out.println(message); + } + } + + private String cleanupPath(String path) { + path = path.trim(); + // No need for current dir path + if (path.startsWith("./")) { + path = path.substring(2); + } + String lowerCasePath = path.toLowerCase(Locale.ENGLISH); + if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) { + return path; + } + if (path.endsWith("/*")) { + path = path.substring(0, path.length() - 1); + } + else { + // It's a directory + if (!path.endsWith("/") && !path.equals(".")) { + path = path + "/"; + } + } + return path; + } + + void close() throws Exception { + if (this.classPathArchives != null) { + this.classPathArchives.close(); + } + if (this.parent != null) { + this.parent.close(); + } + } + + /** + * An iterable collection of the classpath archives. + */ + private class ClassPathArchives implements Iterable { + + private final List classPathArchives; + + private final List jarFileArchives = new ArrayList<>(); + + ClassPathArchives() throws Exception { + this.classPathArchives = new ArrayList<>(); + for (String path : PropertiesLauncher.this.paths) { + for (Archive archive : getClassPathArchives(path)) { + addClassPathArchive(archive); + } + } + addNestedEntries(); + } + + private void addClassPathArchive(Archive archive) throws IOException { + if (!(archive instanceof ExplodedArchive)) { + this.classPathArchives.add(archive); + return; + } + this.classPathArchives.add(archive); + this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter()))); + } + + private List getClassPathArchives(String path) throws Exception { + String root = cleanupPath(handleUrl(path)); + List lib = new ArrayList<>(); + File file = new File(root); + if (!"/".equals(root)) { + if (!isAbsolutePath(root)) { + file = new File(PropertiesLauncher.this.home, root); + } + if (file.isDirectory()) { + debug("Adding classpath entries from " + file); + Archive archive = new ExplodedArchive(file, false); + lib.add(archive); + } + } + Archive archive = getArchive(file); + if (archive != null) { + debug("Adding classpath entries from archive " + archive.getUrl() + root); + lib.add(archive); + } + List nestedArchives = getNestedArchives(root); + if (nestedArchives != null) { + debug("Adding classpath entries from nested " + root); + lib.addAll(nestedArchives); + } + return lib; + } + + private boolean isAbsolutePath(String root) { + // Windows contains ":" others start with "/" + return root.contains(":") || root.startsWith("/"); + } + + private Archive getArchive(File file) throws IOException { + if (isNestedArchivePath(file)) { + return null; + } + String name = file.getName().toLowerCase(Locale.ENGLISH); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return getJarFileArchive(file); + } + return null; + } + + private boolean isNestedArchivePath(File file) { + return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR); + } + + private List getNestedArchives(String path) throws Exception { + Archive parent = PropertiesLauncher.this.parent; + String root = path; + if (!root.equals("/") && root.startsWith("/") + || parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) { + // If home dir is same as parent archive, no need to add it twice. + return null; + } + int index = root.indexOf('!'); + if (index != -1) { + File file = new File(PropertiesLauncher.this.home, root.substring(0, index)); + if (root.startsWith("jar:file:")) { + file = new File(root.substring("jar:file:".length(), index)); + } + parent = getJarFileArchive(file); + root = root.substring(index + 1); + while (root.startsWith("/")) { + root = root.substring(1); + } + } + if (root.endsWith(".jar")) { + File file = new File(PropertiesLauncher.this.home, root); + if (file.exists()) { + parent = getJarFileArchive(file); + root = ""; + } + } + if (root.equals("/") || root.equals("./") || root.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + root = ""; + } + EntryFilter filter = new PrefixMatchingArchiveFilter(root); + List archives = asList(parent.getNestedArchives(null, filter)); + if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar") + && parent != PropertiesLauncher.this.parent) { + // You can't find the root with an entry filter so it has to be added + // explicitly. But don't add the root of the parent archive. + archives.add(parent); + } + return archives; + } + + private void addNestedEntries() { + // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" + // directories, meaning we are running from an executable JAR. We add nested + // entries from there with low priority (i.e. at end). + try { + Iterator archives = PropertiesLauncher.this.parent.getNestedArchives(null, + JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER); + while (archives.hasNext()) { + this.classPathArchives.add(archives.next()); + } + } + catch (IOException ex) { + // Ignore + } + } + + private List asList(Iterator iterator) { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return list; + } + + private JarFileArchive getJarFileArchive(File file) throws IOException { + JarFileArchive archive = new JarFileArchive(file); + this.jarFileArchives.add(archive); + return archive; + } + + @Override + public Iterator iterator() { + return this.classPathArchives.iterator(); + } + + void close() throws IOException { + for (JarFileArchive archive : this.jarFileArchives) { + archive.close(); + } + } + + } + + /** + * Convenience class for finding nested archives that have a prefix in their file path + * (e.g. "lib/"). + */ + private static final class PrefixMatchingArchiveFilter implements EntryFilter { + + private final String prefix; + + private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); + + private PrefixMatchingArchiveFilter(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean matches(Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals(this.prefix); + } + return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); + } + + } + + /** + * Convenience class for finding nested archives (archive entries that can be + * classpath entries). + */ + private static final class ArchiveEntryFilter implements EntryFilter { + + private static final String DOT_JAR = ".jar"; + + private static final String DOT_ZIP = ".zip"; + + @Override + public boolean matches(Entry entry) { + return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java new file mode 100644 index 00000000000..482832c1f72 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import org.springframework.boot.loader.archive.Archive; + +/** + * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. + * Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided}, + * classes are loaded from {@code WEB-INF/classes}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick + * @since 1.0.0 + */ +public class WarLauncher extends ExecutableArchiveLauncher { + + public WarLauncher() { + } + + protected WarLauncher(Archive archive) { + super(archive); + } + + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + public boolean isNestedArchive(Archive.Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals("WEB-INF/classes/"); + } + return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/"); + } + + @Override + protected String getArchiveEntryPathPrefix() { + return "WEB-INF/"; + } + + public static void main(String[] args) throws Exception { + new WarLauncher().launch(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java new file mode 100644 index 00000000000..c1f2bbb2f75 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.archive; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Iterator; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.Launcher; + +/** + * An archive that can be launched by the {@link Launcher}. + * + * @author Phillip Webb + * @since 1.0.0 + * @see JarFileArchive + */ +public interface Archive extends Iterable, AutoCloseable { + + /** + * Returns a URL that can be used to load the archive. + * @return the archive URL + * @throws MalformedURLException if the URL is malformed + */ + URL getUrl() throws MalformedURLException; + + /** + * Returns the manifest of the archive. + * @return the manifest + * @throws IOException if the manifest cannot be read + */ + Manifest getManifest() throws IOException; + + /** + * Returns nested {@link Archive}s for entries that match the specified filters. + * @param searchFilter filter used to limit when additional sub-entry searching is + * required or {@code null} if all entries should be considered. + * @param includeFilter filter used to determine which entries should be included in + * the result or {@code null} if all entries should be included + * @return the nested archives + * @throws IOException on IO error + * @since 2.3.0 + */ + Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException; + + /** + * Return if the archive is exploded (already unpacked). + * @return if the archive is exploded + * @since 2.3.0 + */ + default boolean isExploded() { + return false; + } + + /** + * Closes the {@code Archive}, releasing any open resources. + * @throws Exception if an error occurs during close processing + * @since 2.2.0 + */ + @Override + default void close() throws Exception { + + } + + /** + * Represents a single entry in the archive. + */ + interface Entry { + + /** + * Returns {@code true} if the entry represents a directory. + * @return if the entry is a directory + */ + boolean isDirectory(); + + /** + * Returns the name of the entry. + * @return the name of the entry + */ + String getName(); + + } + + /** + * Strategy interface to filter {@link Entry Entries}. + */ + @FunctionalInterface + interface EntryFilter { + + /** + * Apply the jar entry filter. + * @param entry the entry to filter + * @return {@code true} if the filter matches + */ + boolean matches(Entry entry); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java new file mode 100644 index 00000000000..f8cd52dc16f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -0,0 +1,342 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.archive; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.jar.Manifest; + +/** + * {@link Archive} implementation backed by an exploded archive directory. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @since 1.0.0 + */ +public class ExplodedArchive implements Archive { + + private static final Set SKIPPED_NAMES = new HashSet<>(Arrays.asList(".", "..")); + + private final File root; + + private final boolean recursive; + + private final File manifestFile; + + private Manifest manifest; + + /** + * Create a new {@link ExplodedArchive} instance. + * @param root the root directory + */ + public ExplodedArchive(File root) { + this(root, true); + } + + /** + * Create a new {@link ExplodedArchive} instance. + * @param root the root directory + * @param recursive if recursive searching should be used to locate the manifest. + * Defaults to {@code true}, directories with a large tree might want to set this to + * {@code false}. + */ + public ExplodedArchive(File root, boolean recursive) { + if (!root.exists() || !root.isDirectory()) { + throw new IllegalArgumentException("Invalid source directory " + root); + } + this.root = root; + this.recursive = recursive; + this.manifestFile = getManifestFile(root); + } + + private File getManifestFile(File root) { + File metaInf = new File(root, "META-INF"); + return new File(metaInf, "MANIFEST.MF"); + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.root.toURI().toURL(); + } + + @Override + public Manifest getManifest() throws IOException { + if (this.manifest == null && this.manifestFile.exists()) { + try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) { + this.manifest = new Manifest(inputStream); + } + } + return this.manifest; + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return new EntryIterator(this.root, this.recursive, null, null); + } + + protected Archive getNestedArchive(Entry entry) { + File file = ((FileEntry) entry).getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry)); + } + + @Override + public boolean isExploded() { + return true; + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "exploded archive"; + } + } + + /** + * File based {@link Entry} {@link Iterator}. + */ + private abstract static class AbstractIterator implements Iterator { + + private static final Comparator entryComparator = Comparator.comparing(File::getAbsolutePath); + + private final File root; + + private final boolean recursive; + + private final EntryFilter searchFilter; + + private final EntryFilter includeFilter; + + private final Deque> stack = new LinkedList<>(); + + private FileEntry current; + + private final String rootUrl; + + AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + this.root = root; + this.rootUrl = this.root.toURI().getPath(); + this.recursive = recursive; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; + this.stack.add(listFiles(root)); + this.current = poll(); + } + + @Override + public boolean hasNext() { + return this.current != null; + } + + @Override + public T next() { + FileEntry entry = this.current; + if (entry == null) { + throw new NoSuchElementException(); + } + this.current = poll(); + return adapt(entry); + } + + private FileEntry poll() { + while (!this.stack.isEmpty()) { + while (this.stack.peek().hasNext()) { + File file = this.stack.peek().next(); + if (SKIPPED_NAMES.contains(file.getName())) { + continue; + } + FileEntry entry = getFileEntry(file); + if (isListable(entry)) { + this.stack.addFirst(listFiles(file)); + } + if (this.includeFilter == null || this.includeFilter.matches(entry)) { + return entry; + } + } + this.stack.poll(); + } + return null; + } + + private FileEntry getFileEntry(File file) { + URI uri = file.toURI(); + String name = uri.getPath().substring(this.rootUrl.length()); + try { + return new FileEntry(name, file, uri.toURL()); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private boolean isListable(FileEntry entry) { + return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root)) + && (this.searchFilter == null || this.searchFilter.matches(entry)) + && (this.includeFilter == null || !this.includeFilter.matches(entry)); + } + + private Iterator listFiles(File file) { + File[] files = file.listFiles(); + if (files == null) { + return Collections.emptyIterator(); + } + Arrays.sort(files, entryComparator); + return Arrays.asList(files).iterator(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + protected abstract T adapt(FileEntry entry); + + } + + private static class EntryIterator extends AbstractIterator { + + EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + + @Override + protected Entry adapt(FileEntry entry) { + return entry; + } + + } + + private static class ArchiveIterator extends AbstractIterator { + + ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + + @Override + protected Archive adapt(FileEntry entry) { + File file = entry.getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry)); + } + + } + + /** + * {@link Entry} backed by a File. + */ + private static class FileEntry implements Entry { + + private final String name; + + private final File file; + + private final URL url; + + FileEntry(String name, File file, URL url) { + this.name = name; + this.file = file; + this.url = url; + } + + File getFile() { + return this.file; + } + + @Override + public boolean isDirectory() { + return this.file.isDirectory(); + } + + @Override + public String getName() { + return this.name; + } + + URL getUrl() { + return this.url; + } + + } + + /** + * {@link Archive} implementation backed by a simple JAR file that doesn't itself + * contain nested archives. + */ + private static class SimpleJarFileArchive implements Archive { + + private final URL url; + + SimpleJarFileArchive(FileEntry file) { + this.url = file.getUrl(); + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.url; + } + + @Override + public Manifest getManifest() throws IOException { + return null; + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) + throws IOException { + return Collections.emptyIterator(); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "jar archive"; + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java new file mode 100755 index 00000000000..91e7bc53a48 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -0,0 +1,310 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.archive; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.jar.JarFile; + +/** + * {@link Archive} implementation backed by a {@link JarFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class JarFileArchive implements Archive { + + private static final String UNPACK_MARKER = "UNPACK:"; + + private static final int BUFFER_SIZE = 32 * 1024; + + private static final FileAttribute[] NO_FILE_ATTRIBUTES = {}; + + private static final EnumSet DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE); + + private static final EnumSet FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE); + + private final JarFile jarFile; + + private URL url; + + private Path tempUnpackDirectory; + + public JarFileArchive(File file) throws IOException { + this(file, file.toURI().toURL()); + } + + public JarFileArchive(File file, URL url) throws IOException { + this(new JarFile(file)); + this.url = url; + } + + public JarFileArchive(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + public URL getUrl() throws MalformedURLException { + if (this.url != null) { + return this.url; + } + return this.jarFile.getUrl(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.jarFile.getManifest(); + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return new EntryIterator(this.jarFile.iterator(), null, null); + } + + @Override + public void close() throws IOException { + this.jarFile.close(); + } + + protected Archive getNestedArchive(Entry entry) throws IOException { + JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); + if (jarEntry.getComment().startsWith(UNPACK_MARKER)) { + return getUnpackedNestedArchive(jarEntry); + } + try { + JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + return new JarFileArchive(jarFile); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex); + } + } + + private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { + String name = jarEntry.getName(); + if (name.lastIndexOf('/') != -1) { + name = name.substring(name.lastIndexOf('/') + 1); + } + Path path = getTempUnpackDirectory().resolve(name); + if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { + unpack(jarEntry, path); + } + return new JarFileArchive(path.toFile(), path.toUri().toURL()); + } + + private Path getTempUnpackDirectory() { + if (this.tempUnpackDirectory == null) { + Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + this.tempUnpackDirectory = createUnpackDirectory(tempDirectory); + } + return this.tempUnpackDirectory; + } + + private Path createUnpackDirectory(Path parent) { + int attempts = 0; + while (attempts++ < 1000) { + String fileName = Paths.get(this.jarFile.getName()).getFileName().toString(); + Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID()); + try { + createDirectory(unpackDirectory); + return unpackDirectory; + } + catch (IOException ex) { + } + } + throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'"); + } + + private void unpack(JarEntry entry, Path path) throws IOException { + createFile(path); + path.toFile().deleteOnExit(); + try (InputStream inputStream = this.jarFile.getInputStream(entry); + OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING)) { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } + } + + private void createDirectory(Path path) throws IOException { + Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS)); + } + + private void createFile(Path path) throws IOException { + Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS)); + } + + private FileAttribute[] getFileAttributes(FileSystem fileSystem, EnumSet ownerReadWrite) { + if (!fileSystem.supportedFileAttributeViews().contains("posix")) { + return NO_FILE_ATTRIBUTES; + } + return new FileAttribute[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) }; + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "jar archive"; + } + } + + /** + * Abstract base class for iterator implementations. + */ + private abstract static class AbstractIterator implements Iterator { + + private final Iterator iterator; + + private final EntryFilter searchFilter; + + private final EntryFilter includeFilter; + + private Entry current; + + AbstractIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + this.iterator = iterator; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; + this.current = poll(); + } + + @Override + public boolean hasNext() { + return this.current != null; + } + + @Override + public T next() { + T result = adapt(this.current); + this.current = poll(); + return result; + } + + private Entry poll() { + while (this.iterator.hasNext()) { + JarFileEntry candidate = new JarFileEntry(this.iterator.next()); + if ((this.searchFilter == null || this.searchFilter.matches(candidate)) + && (this.includeFilter == null || this.includeFilter.matches(candidate))) { + return candidate; + } + } + return null; + } + + protected abstract T adapt(Entry entry); + + } + + /** + * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. + */ + private static class EntryIterator extends AbstractIterator { + + EntryIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); + } + + @Override + protected Entry adapt(Entry entry) { + return entry; + } + + } + + /** + * Nested {@link Archive} iterator implementation backed by {@link JarEntry}. + */ + private class NestedArchiveIterator extends AbstractIterator { + + NestedArchiveIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); + } + + @Override + protected Archive adapt(Entry entry) { + try { + return getNestedArchive(entry); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + } + + /** + * {@link Archive.Entry} implementation backed by a {@link JarEntry}. + */ + private static class JarFileEntry implements Entry { + + private final JarEntry jarEntry; + + JarFileEntry(JarEntry jarEntry) { + this.jarEntry = jarEntry; + } + + JarEntry getJarEntry() { + return this.jarEntry; + } + + @Override + public boolean isDirectory() { + return this.jarEntry.isDirectory(); + } + + @Override + public String getName() { + return this.jarEntry.getName(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java new file mode 100644 index 00000000000..27ce99b006f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/** + * Abstraction over logical Archives be they backed by a JAR file or unpacked into a + * directory. + * + * @see org.springframework.boot.loader.archive.Archive + */ +package org.springframework.boot.loader.archive; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java new file mode 100644 index 00000000000..e96d5ea81a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.data; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface that provides read-only random access to some underlying data. + * Implementations must allow concurrent reads in a thread-safe manner. + * + * @author Phillip Webb + * @since 1.0.0 + */ +public interface RandomAccessData { + + /** + * Returns an {@link InputStream} that can be used to read the underlying data. The + * caller is responsible close the underlying stream. + * @return a new input stream that can be used to read the underlying data. + * @throws IOException if the stream cannot be opened + */ + InputStream getInputStream() throws IOException; + + /** + * Returns a new {@link RandomAccessData} for a specific subsection of this data. + * @param offset the offset of the subsection + * @param length the length of the subsection + * @return the subsection data + */ + RandomAccessData getSubsection(long offset, long length); + + /** + * Reads all the data and returns it as a byte array. + * @return the data + * @throws IOException if the data cannot be read + */ + byte[] read() throws IOException; + + /** + * Reads the {@code length} bytes of data starting at the given {@code offset}. + * @param offset the offset from which data should be read + * @param length the number of bytes to be read + * @return the data + * @throws IOException if the data cannot be read + * @throws IndexOutOfBoundsException if offset is beyond the end of the file or + * subsection + * @throws EOFException if offset plus length is greater than the length of the file + * or subsection + */ + byte[] read(long offset, long length) throws IOException; + + /** + * Returns the size of the data. + * @return the size + */ + long getSize(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java new file mode 100644 index 00000000000..4bd5d205418 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -0,0 +1,262 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.data; + +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class RandomAccessDataFile implements RandomAccessData { + + private final FileAccess fileAccess; + + private final long offset; + + private final long length; + + /** + * Create a new {@link RandomAccessDataFile} backed by the specified file. + * @param file the underlying file + * @throws IllegalArgumentException if the file is null or does not exist + */ + public RandomAccessDataFile(File file) { + if (file == null) { + throw new IllegalArgumentException("File must not be null"); + } + this.fileAccess = new FileAccess(file); + this.offset = 0L; + this.length = file.length(); + } + + /** + * Private constructor used to create a {@link #getSubsection(long, long) subsection}. + * @param fileAccess provides access to the underlying file + * @param offset the offset of the section + * @param length the length of the section + */ + private RandomAccessDataFile(FileAccess fileAccess, long offset, long length) { + this.fileAccess = fileAccess; + this.offset = offset; + this.length = length; + } + + /** + * Returns the underlying File. + * @return the underlying file + */ + public File getFile() { + return this.fileAccess.file; + } + + @Override + public InputStream getInputStream() throws IOException { + return new DataInputStream(); + } + + @Override + public RandomAccessData getSubsection(long offset, long length) { + if (offset < 0 || length < 0 || offset + length > this.length) { + throw new IndexOutOfBoundsException(); + } + return new RandomAccessDataFile(this.fileAccess, this.offset + offset, length); + } + + @Override + public byte[] read() throws IOException { + return read(0, this.length); + } + + @Override + public byte[] read(long offset, long length) throws IOException { + if (offset > this.length) { + throw new IndexOutOfBoundsException(); + } + if (offset + length > this.length) { + throw new EOFException(); + } + byte[] bytes = new byte[(int) length]; + read(bytes, offset, 0, bytes.length); + return bytes; + } + + private int readByte(long position) throws IOException { + if (position >= this.length) { + return -1; + } + return this.fileAccess.readByte(this.offset + position); + } + + private int read(byte[] bytes, long position, int offset, int length) throws IOException { + if (position > this.length) { + return -1; + } + return this.fileAccess.read(bytes, this.offset + position, offset, length); + } + + @Override + public long getSize() { + return this.length; + } + + public void close() throws IOException { + this.fileAccess.close(); + } + + /** + * {@link InputStream} implementation for the {@link RandomAccessDataFile}. + */ + private class DataInputStream extends InputStream { + + private int position; + + @Override + public int read() throws IOException { + int read = RandomAccessDataFile.this.readByte(this.position); + if (read > -1) { + moveOn(1); + } + return read; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, (b != null) ? b.length : 0); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("Bytes must not be null"); + } + return doRead(b, off, len); + } + + /** + * Perform the actual read. + * @param b the bytes to read or {@code null} when reading a single byte + * @param off the offset of the byte array + * @param len the length of data to read + * @return the number of bytes read into {@code b} or the actual read byte if + * {@code b} is {@code null}. Returns -1 when the end of the stream is reached + * @throws IOException in case of I/O errors + */ + int doRead(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int cappedLen = cap(len); + if (cappedLen <= 0) { + return -1; + } + return (int) moveOn(RandomAccessDataFile.this.read(b, this.position, off, cappedLen)); + } + + @Override + public long skip(long n) throws IOException { + return (n <= 0) ? 0 : moveOn(cap(n)); + } + + @Override + public int available() throws IOException { + return (int) RandomAccessDataFile.this.length - this.position; + } + + /** + * Cap the specified value such that it cannot exceed the number of bytes + * remaining. + * @param n the value to cap + * @return the capped value + */ + private int cap(long n) { + return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); + } + + /** + * Move the stream position forwards the specified amount. + * @param amount the amount to move + * @return the amount moved + */ + private long moveOn(int amount) { + this.position += amount; + return amount; + } + + } + + private static final class FileAccess { + + private final Object monitor = new Object(); + + private final File file; + + private RandomAccessFile randomAccessFile; + + private FileAccess(File file) { + this.file = file; + openIfNecessary(); + } + + private int read(byte[] bytes, long position, int offset, int length) throws IOException { + synchronized (this.monitor) { + openIfNecessary(); + this.randomAccessFile.seek(position); + return this.randomAccessFile.read(bytes, offset, length); + } + } + + private void openIfNecessary() { + if (this.randomAccessFile == null) { + try { + this.randomAccessFile = new RandomAccessFile(this.file, "r"); + } + catch (FileNotFoundException ex) { + throw new IllegalArgumentException( + String.format("File %s must exist", this.file.getAbsolutePath())); + } + } + } + + private void close() throws IOException { + synchronized (this.monitor) { + if (this.randomAccessFile != null) { + this.randomAccessFile.close(); + this.randomAccessFile = null; + } + } + } + + private int readByte(long position) throws IOException { + synchronized (this.monitor) { + openIfNecessary(); + this.randomAccessFile.seek(position); + return this.randomAccessFile.read(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java new file mode 100644 index 00000000000..34bf2ead437 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/** + * Classes and interfaces to allow random access to a block of data. + * + * @see org.springframework.boot.loader.data.RandomAccessData + */ +package org.springframework.boot.loader.data; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java new file mode 100644 index 00000000000..6a98ef68218 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; + +/** + * Base class for extended variants of {@link java.util.jar.JarFile}. + * + * @author Phillip Webb + */ +abstract class AbstractJarFile extends java.util.jar.JarFile { + + /** + * Create a new {@link AbstractJarFile}. + * @param file the root jar file. + * @throws IOException on IO error + */ + AbstractJarFile(File file) throws IOException { + super(file); + } + + /** + * Return a URL that can be used to access this JAR file. NOTE: the specified URL + * cannot be serialized and or cloned. + * @return the URL + * @throws MalformedURLException if the URL is malformed + */ + abstract URL getUrl() throws MalformedURLException; + + /** + * Return the {@link JarFileType} of this instance. + * @return the jar file type + */ + abstract JarFileType getType(); + + /** + * Return the security permission for this JAR. + * @return the security permission. + */ + abstract Permission getPermission(); + + /** + * Return an {@link InputStream} for the entire jar contents. + * @return the contents input stream + * @throws IOException on IO error + */ + abstract InputStream getInputStream() throws IOException; + + /** + * The type of a {@link JarFile}. + */ + enum JarFileType { + + DIRECT, NESTED_DIRECTORY, NESTED_JAR + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java new file mode 100644 index 00000000000..cfe121b6899 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -0,0 +1,255 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.nio.charset.StandardCharsets; + +/** + * Simple wrapper around a byte array that represents an ASCII. Used for performance + * reasons to save constructing Strings for ZIP data. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class AsciiBytes { + + private static final String EMPTY_STRING = ""; + + private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 }; + + private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F; + + private final byte[] bytes; + + private final int offset; + + private final int length; + + private String string; + + private int hash; + + /** + * Create a new {@link AsciiBytes} from the specified String. + * @param string the source string + */ + AsciiBytes(String string) { + this(string.getBytes(StandardCharsets.UTF_8)); + this.string = string; + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the source bytes + */ + AsciiBytes(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the source bytes + * @param offset the offset + * @param length the length + */ + AsciiBytes(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || (offset + length) > bytes.length) { + throw new IndexOutOfBoundsException(); + } + this.bytes = bytes; + this.offset = offset; + this.length = length; + } + + int length() { + return this.length; + } + + boolean startsWith(AsciiBytes prefix) { + if (this == prefix) { + return true; + } + if (prefix.length > this.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) { + return false; + } + } + return true; + } + + boolean endsWith(AsciiBytes postfix) { + if (this == postfix) { + return true; + } + if (postfix.length > this.length) { + return false; + } + for (int i = 0; i < postfix.length; i++) { + if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + (postfix.length - 1) + - i]) { + return false; + } + } + return true; + } + + AsciiBytes substring(int beginIndex) { + return substring(beginIndex, this.length); + } + + AsciiBytes substring(int beginIndex, int endIndex) { + int length = endIndex - beginIndex; + if (this.offset + length > this.bytes.length) { + throw new IndexOutOfBoundsException(); + } + return new AsciiBytes(this.bytes, this.offset + beginIndex, length); + } + + boolean matches(CharSequence name, char suffix) { + int charIndex = 0; + int nameLen = name.length(); + int totalLen = nameLen + ((suffix != 0) ? 1 : 0); + for (int i = this.offset; i < this.offset + this.length; i++) { + int b = this.bytes[i]; + int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; + b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; + for (int j = 0; j < remainingUtfBytes; j++) { + b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); + } + char c = getChar(name, suffix, charIndex++); + if (b <= 0xFFFF) { + if (c != b) { + return false; + } + } + else { + if (c != ((b >> 0xA) + 0xD7C0)) { + return false; + } + c = getChar(name, suffix, charIndex++); + if (c != ((b & 0x3FF) + 0xDC00)) { + return false; + } + } + } + return charIndex == totalLen; + } + + private char getChar(CharSequence name, char suffix, int index) { + if (index < name.length()) { + return name.charAt(index); + } + if (index == name.length()) { + return suffix; + } + return 0; + } + + private int getNumberOfUtfBytes(int b) { + if ((b & 0x80) == 0) { + return 1; + } + int numberOfUtfBytes = 0; + while ((b & 0x80) != 0) { + b <<= 1; + numberOfUtfBytes++; + } + return numberOfUtfBytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj.getClass() == AsciiBytes.class) { + AsciiBytes other = (AsciiBytes) obj; + if (this.length == other.length) { + for (int i = 0; i < this.length; i++) { + if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) { + return false; + } + } + return true; + } + } + return false; + } + + @Override + public int hashCode() { + int hash = this.hash; + if (hash == 0 && this.bytes.length > 0) { + for (int i = this.offset; i < this.offset + this.length; i++) { + int b = this.bytes[i]; + int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; + b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; + for (int j = 0; j < remainingUtfBytes; j++) { + b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); + } + if (b <= 0xFFFF) { + hash = 31 * hash + b; + } + else { + hash = 31 * hash + ((b >> 0xA) + 0xD7C0); + hash = 31 * hash + ((b & 0x3FF) + 0xDC00); + } + } + this.hash = hash; + } + return hash; + } + + @Override + public String toString() { + if (this.string == null) { + if (this.length == 0) { + this.string = EMPTY_STRING; + } + else { + this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8); + } + } + return this.string; + } + + static String toString(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + + static int hashCode(CharSequence charSequence) { + // We're compatible with String's hashCode() + if (charSequence instanceof StringSequence) { + // ... but save making an unnecessary String for StringSequence + return charSequence.hashCode(); + } + return charSequence.toString().hashCode(); + } + + static int hashCode(int hash, char suffix) { + return (suffix != 0) ? (31 * hash + suffix) : hash; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java new file mode 100644 index 00000000000..d46a22555dc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +/** + * Utilities for dealing with bytes from ZIP files. + * + * @author Phillip Webb + */ +final class Bytes { + + private Bytes() { + } + + static long littleEndianValue(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = length - 1; i >= 0; i--) { + value = ((value << 8) | (bytes[offset + i] & 0xFF)); + } + return value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java new file mode 100644 index 00000000000..61db0b73f42 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -0,0 +1,258 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * A ZIP File "End of central directory record" (EOCD). + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Camille Vienot + * @see Zip File Format + */ +class CentralDirectoryEndRecord { + + private static final int MINIMUM_SIZE = 22; + + private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; + + private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; + + private static final int SIGNATURE = 0x06054b50; + + private static final int COMMENT_LENGTH_OFFSET = 20; + + private static final int READ_BLOCK_SIZE = 256; + + private final Zip64End zip64End; + + private byte[] block; + + private int offset; + + private int size; + + /** + * Create a new {@link CentralDirectoryEndRecord} instance from the specified + * {@link RandomAccessData}, searching backwards from the end until a valid block is + * located. + * @param data the source data + * @throws IOException in case of I/O errors + */ + CentralDirectoryEndRecord(RandomAccessData data) throws IOException { + this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE); + this.size = MINIMUM_SIZE; + this.offset = this.block.length - this.size; + while (!isValid()) { + this.size++; + if (this.size > this.block.length) { + if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) { + throw new IOException( + "Unable to find ZIP central directory records after reading " + this.size + " bytes"); + } + this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE); + } + this.offset = this.block.length - this.size; + } + long startOfCentralDirectoryEndRecord = data.getSize() - this.size; + Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord); + this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null; + } + + private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { + int length = (int) Math.min(data.getSize(), size); + return data.read(data.getSize() - length, length); + } + + private boolean isValid() { + if (this.block.length < MINIMUM_SIZE || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) { + return false; + } + // Total size must be the structure size + comment + long commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); + return this.size == MINIMUM_SIZE + commentLength; + } + + /** + * Returns the location in the data that the archive actually starts. For most files + * the archive data will start at 0, however, it is possible to have prefixed bytes + * (often used for startup scripts) at the beginning of the data. + * @param data the source data + * @return the offset within the data where the archive begins + */ + long getStartOfArchive(RandomAccessData data) { + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset + : Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L; + int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0; + long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize; + return actualOffset - specifiedOffset; + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in this + * record. + * @param data the source data + * @return the central directory data + */ + RandomAccessData getCentralDirectory(RandomAccessData data) { + if (this.zip64End != null) { + return this.zip64End.getCentralDirectory(data); + } + long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + return data.getSubsection(offset, length); + } + + /** + * Return the number of ZIP entries in the file. + * @return the number of records in the zip + */ + int getNumberOfRecords() { + if (this.zip64End != null) { + return this.zip64End.getNumberOfRecords(); + } + long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); + return (int) numberOfRecords; + } + + String getComment() { + int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); + AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength); + return comment.toString(); + } + + boolean isZip64() { + return this.zip64End != null; + } + + /** + * A Zip64 end of central directory record. + * + * @see Chapter + * 4.3.14 of Zip64 specification + */ + private static final class Zip64End { + + private static final int ZIP64_ENDTOT = 32; // total number of entries + + private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + + private static final int ZIP64_ENDOFF = 48; // offset of first CEN header + + private final Zip64Locator locator; + + private final long centralDirectoryOffset; + + private final long centralDirectoryLength; + + private final int numberOfRecords; + + private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { + this.locator = locator; + byte[] block = data.read(locator.getZip64EndOffset(), 56); + this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); + this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8); + this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8); + } + + /** + * Return the size of this zip 64 end of central directory record. + * @return size of this zip 64 end of central directory record + */ + private long getSize() { + return this.locator.getZip64EndSize(); + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in + * this record. + * @param data the source data + * @return the central directory data + */ + private RandomAccessData getCentralDirectory(RandomAccessData data) { + return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength); + } + + /** + * Return the number of entries in the zip64 archive. + * @return the number of records in the zip + */ + private int getNumberOfRecords() { + return this.numberOfRecords; + } + + } + + /** + * A Zip64 end of central directory locator. + * + * @see Chapter + * 4.3.15 of Zip64 specification + */ + private static final class Zip64Locator { + + static final int SIGNATURE = 0x07064b50; + + static final int ZIP64_LOCSIZE = 20; // locator size + + static final int ZIP64_LOCOFF = 8; // offset of zip64 end + + private final long zip64EndOffset; + + private final long offset; + + private Zip64Locator(long offset, byte[] block) { + this.offset = offset; + this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); + } + + /** + * Return the size of the zip 64 end record located by this zip64 end locator. + * @return size of the zip 64 end record located by this zip64 end locator + */ + private long getZip64EndSize() { + return this.offset - this.zip64EndOffset; + } + + /** + * Return the offset to locate {@link Zip64End}. + * @return offset of the Zip64 end of central directory record + */ + private long getZip64EndOffset() { + return this.zip64EndOffset; + } + + private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException { + long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; + if (offset >= 0) { + byte[] block = data.read(offset, ZIP64_LOCSIZE); + if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) { + return new Zip64Locator(offset, block); + } + } + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java new file mode 100644 index 00000000000..19c88dda524 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.ValueRange; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * A ZIP File "Central directory file header record" (CDFH). + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Dmytro Nosan + * @see Zip File Format + */ + +final class CentralDirectoryFileHeader implements FileHeader { + + private static final AsciiBytes SLASH = new AsciiBytes("/"); + + private static final byte[] NO_EXTRA = {}; + + private static final AsciiBytes NO_COMMENT = new AsciiBytes(""); + + private byte[] header; + + private int headerOffset; + + private AsciiBytes name; + + private byte[] extra; + + private AsciiBytes comment; + + private long localHeaderOffset; + + CentralDirectoryFileHeader() { + } + + CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, byte[] extra, AsciiBytes comment, + long localHeaderOffset) { + this.header = header; + this.headerOffset = headerOffset; + this.name = name; + this.extra = extra; + this.comment = comment; + this.localHeaderOffset = localHeaderOffset; + } + + void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter) + throws IOException { + // Load fixed part + this.header = data; + this.headerOffset = dataOffset; + long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4); + long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4); + long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2); + long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2); + long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2); + long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); + // Load variable part + dataOffset += 46; + if (variableData != null) { + data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength); + dataOffset = 0; + } + this.name = new AsciiBytes(data, dataOffset, (int) nameLength); + if (filter != null) { + this.name = filter.apply(this.name); + } + this.extra = NO_EXTRA; + this.comment = NO_COMMENT; + if (extraLength > 0) { + this.extra = new byte[(int) extraLength]; + System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length); + } + this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra); + if (commentLength > 0) { + this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength); + } + } + + private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra) + throws IOException { + if (localHeaderOffset != 0xFFFFFFFFL) { + return localHeaderOffset; + } + int extraOffset = 0; + while (extraOffset < extra.length - 2) { + int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + extraOffset += 4; + if (id == 1) { + int localHeaderExtraOffset = 0; + if (compressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + if (uncompressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8); + } + extraOffset += length; + } + throw new IOException("Zip64 Extended Information Extra Field not found"); + } + + AsciiBytes getName() { + return this.name; + } + + @Override + public boolean hasName(CharSequence name, char suffix) { + return this.name.matches(name, suffix); + } + + boolean isDirectory() { + return this.name.endsWith(SLASH); + } + + @Override + public int getMethod() { + return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2); + } + + long getTime() { + long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4); + return decodeMsDosFormatDateTime(datetime); + } + + /** + * Decode MS-DOS Date Time details. See + * Microsoft's documentation for more details of the format. + * @param datetime the date and time + * @return the date and time as milliseconds since the epoch + */ + private long decodeMsDosFormatDateTime(long datetime) { + int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR); + int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR); + int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH); + int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); + int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); + int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS) + .toEpochMilli(); + } + + long getCrc() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4); + } + + @Override + public long getCompressedSize() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4); + } + + @Override + public long getSize() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4); + } + + byte[] getExtra() { + return this.extra; + } + + boolean hasExtra() { + return this.extra.length > 0; + } + + AsciiBytes getComment() { + return this.comment; + } + + @Override + public long getLocalHeaderOffset() { + return this.localHeaderOffset; + } + + @Override + public CentralDirectoryFileHeader clone() { + byte[] header = new byte[46]; + System.arraycopy(this.header, this.headerOffset, header, 0, header.length); + return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset); + } + + static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter) + throws IOException { + CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); + byte[] bytes = data.read(offset, 46); + fileHeader.load(bytes, 0, data, offset, filter); + return fileHeader; + } + + private static int getChronoValue(long value, ChronoField field) { + ValueRange range = field.range(); + return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java new file mode 100644 index 00000000000..eff96a56e2c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Parses the central directory from a JAR file. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @see CentralDirectoryVisitor + */ +class CentralDirectoryParser { + + private static final int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46; + + private final List visitors = new ArrayList<>(); + + T addVisitor(T visitor) { + this.visitors.add(visitor); + return visitor; + } + + /** + * Parse the source data, triggering {@link CentralDirectoryVisitor visitors}. + * @param data the source data + * @param skipPrefixBytes if prefix bytes should be skipped + * @return the actual archive data without any prefix bytes + * @throws IOException on error + */ + RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException { + CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data); + if (skipPrefixBytes) { + data = getArchiveData(endRecord, data); + } + RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data); + visitStart(endRecord, centralDirectoryData); + parseEntries(endRecord, centralDirectoryData); + visitEnd(); + return data; + } + + private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) + throws IOException { + byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize()); + CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); + int dataOffset = 0; + for (int i = 0; i < endRecord.getNumberOfRecords(); i++) { + fileHeader.load(bytes, dataOffset, null, 0, null); + visitFileHeader(dataOffset, fileHeader); + dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length() + + fileHeader.getComment().length() + fileHeader.getExtra().length; + } + } + + private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, RandomAccessData data) { + long offset = endRecord.getStartOfArchive(data); + if (offset == 0) { + return data; + } + return data.getSubsection(offset, data.getSize() - offset); + } + + private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitStart(endRecord, centralDirectoryData); + } + } + + private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitFileHeader(fileHeader, dataOffset); + } + } + + private void visitEnd() { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitEnd(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java new file mode 100644 index 00000000000..22e04b329c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Callback visitor triggered by {@link CentralDirectoryParser}. + * + * @author Phillip Webb + */ +interface CentralDirectoryVisitor { + + void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData); + + void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset); + + void visitEnd(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java new file mode 100644 index 00000000000..7e4134fe564 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.util.zip.ZipEntry; + +/** + * A file header record that has been loaded from a Jar file. + * + * @author Phillip Webb + * @see JarEntry + * @see CentralDirectoryFileHeader + */ +interface FileHeader { + + /** + * Returns {@code true} if the header has the given name. + * @param name the name to test + * @param suffix an additional suffix (or {@code 0}) + * @return {@code true} if the header has the given name + */ + boolean hasName(CharSequence name, char suffix); + + /** + * Return the offset of the load file header within the archive data. + * @return the local header offset + */ + long getLocalHeaderOffset(); + + /** + * Return the compressed size of the entry. + * @return the compressed size. + */ + long getCompressedSize(); + + /** + * Return the uncompressed size of the entry. + * @return the uncompressed size. + */ + long getSize(); + + /** + * Return the method used to compress the data. + * @return the zip compression method + * @see ZipEntry#STORED + * @see ZipEntry#DEFLATED + */ + int getMethod(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java new file mode 100644 index 00000000000..932dea65486 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -0,0 +1,466 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + * @see JarFile#registerUrlProtocolHandler() + */ +public class Handler extends URLStreamHandler { + + // NOTE: in order to be found as a URL protocol handler, this class must be public, + // must be named Handler and must be in a package ending '.jar' + + private static final String JAR_PROTOCOL = "jar:"; + + private static final String FILE_PROTOCOL = "file:"; + + private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:"; + + private static final String SEPARATOR = "!/"; + + private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL); + + private static final String CURRENT_DIR = "/./"; + + private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL); + + private static final String PARENT_DIR = "/../"; + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; + + private static URL jarContextUrl; + + private static SoftReference> rootFileCache; + + static { + rootFileCache = new SoftReference<>(null); + } + + private final JarFile jarFile; + + private URLStreamHandler fallbackHandler; + + public Handler() { + this(null); + } + + public Handler(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) { + return JarURLConnection.get(url, this.jarFile); + } + try { + return JarURLConnection.get(url, getRootJarFileFromUrl(url)); + } + catch (Exception ex) { + return openFallbackConnection(url, ex); + } + } + + private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLException { + // Try the path first to save building a new url string each time + return url.getPath().startsWith(jarFile.getUrl().getPath()) + && url.toString().startsWith(jarFile.getUrlString()); + } + + private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { + try { + URLConnection connection = openFallbackTomcatConnection(url); + connection = (connection != null) ? connection : openFallbackContextConnection(url); + return (connection != null) ? connection : openFallbackHandlerConnection(url); + } + catch (Exception ex) { + if (reason instanceof IOException ioException) { + log(false, "Unable to open fallback handler", ex); + throw ioException; + } + log(true, "Unable to open fallback handler", ex); + if (reason instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(reason); + } + } + + /** + * Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to + * use our own nested JAR support to open the content rather than the logic in + * {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to + * the temp folder to that its content can be accessed. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackTomcatConnection(URL url) { + String file = url.getFile(); + if (isTomcatWarUrl(file)) { + file = file.substring(TOMCAT_WARFILE_PROTOCOL.length()); + file = file.replaceFirst("\\*/", "!/"); + try { + URLConnection connection = openConnection(new URL("jar:file:" + file)); + connection.getInputStream().close(); + return connection; + } + catch (IOException ex) { + } + } + return null; + } + + private boolean isTomcatWarUrl(String file) { + if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) { + try { + URLConnection connection = new URL(file).openConnection(); + if (connection.getClass().getName().startsWith("org.apache.catalina")) { + return true; + } + } + catch (Exception ex) { + } + } + return false; + } + + /** + * Attempt to open a fallback connection by using a context URL captured before the + * jar handler was replaced with our own version. Since this method doesn't use + * reflection it won't trigger "illegal reflective access operation has occurred" + * warnings on Java 13+. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackContextConnection(URL url) { + try { + if (jarContextUrl != null) { + return new URL(jarContextUrl, url.toExternalForm()).openConnection(); + } + } + catch (Exception ex) { + } + return null; + } + + /** + * Attempt to open a fallback connection by using reflection to access Java's default + * jar {@link URLStreamHandler}. + * @param url the URL to open + * @return the {@link URLConnection} + * @throws Exception if not connection could be opened + */ + private URLConnection openFallbackHandlerConnection(URL url) throws Exception { + URLStreamHandler fallbackHandler = getFallbackHandler(); + return new URL(null, url.toExternalForm(), fallbackHandler).openConnection(); + } + + private URLStreamHandler getFallbackHandler() { + if (this.fallbackHandler != null) { + return this.fallbackHandler; + } + for (String handlerClassName : FALLBACK_HANDLERS) { + try { + Class handlerClass = Class.forName(handlerClassName); + this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance(); + return this.fallbackHandler; + } + catch (Exception ex) { + // Ignore + } + } + throw new IllegalStateException("Unable to find fallback handler"); + } + + private void log(boolean warning, String message, Exception cause) { + try { + Level level = warning ? Level.WARNING : Level.FINEST; + Logger.getLogger(getClass().getName()).log(level, message, cause); + } + catch (Exception ex) { + if (warning) { + System.err.println("WARNING: " + message); + } + } + } + + @Override + protected void parseURL(URL context, String spec, int start, int limit) { + if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) { + setFile(context, getFileFromSpec(spec.substring(start, limit))); + } + else { + setFile(context, getFileFromContext(context, spec.substring(start, limit))); + } + } + + private String getFileFromSpec(String spec) { + int separatorIndex = spec.lastIndexOf("!/"); + if (separatorIndex == -1) { + throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); + } + try { + new URL(spec.substring(0, separatorIndex)); + return spec; + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); + } + } + + private String getFileFromContext(URL context, String spec) { + String file = context.getFile(); + if (spec.startsWith("/")) { + return trimToJarRoot(file) + SEPARATOR + spec.substring(1); + } + if (file.endsWith("/")) { + return file + spec; + } + int lastSlashIndex = file.lastIndexOf('/'); + if (lastSlashIndex == -1) { + throw new IllegalArgumentException("No / found in context URL's file '" + file + "'"); + } + return file.substring(0, lastSlashIndex + 1) + spec; + } + + private String trimToJarRoot(String file) { + int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); + if (lastSeparatorIndex == -1) { + throw new IllegalArgumentException("No !/ found in context URL's file '" + file + "'"); + } + return file.substring(0, lastSeparatorIndex); + } + + private void setFile(URL context, String file) { + String path = normalize(file); + String query = null; + int queryIndex = path.lastIndexOf('?'); + if (queryIndex != -1) { + query = path.substring(queryIndex + 1); + path = path.substring(0, queryIndex); + } + setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, context.getRef()); + } + + private String normalize(String file) { + if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) { + return file; + } + int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); + String afterSeparator = file.substring(afterLastSeparatorIndex); + afterSeparator = replaceParentDir(afterSeparator); + afterSeparator = replaceCurrentDir(afterSeparator); + return file.substring(0, afterLastSeparatorIndex) + afterSeparator; + } + + private String replaceParentDir(String file) { + int parentDirIndex; + while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) { + int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); + if (precedingSlashIndex >= 0) { + file = file.substring(0, precedingSlashIndex) + file.substring(parentDirIndex + 3); + } + else { + file = file.substring(parentDirIndex + 4); + } + } + return file; + } + + private String replaceCurrentDir(String file) { + return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/"); + } + + @Override + protected int hashCode(URL u) { + return hashCode(u.getProtocol(), u.getFile()); + } + + private int hashCode(String protocol, String file) { + int result = (protocol != null) ? protocol.hashCode() : 0; + int separatorIndex = file.indexOf(SEPARATOR); + if (separatorIndex == -1) { + return result + file.hashCode(); + } + String source = file.substring(0, separatorIndex); + String entry = canonicalize(file.substring(separatorIndex + 2)); + try { + result += new URL(source).hashCode(); + } + catch (MalformedURLException ex) { + result += source.hashCode(); + } + result += entry.hashCode(); + return result; + } + + @Override + protected boolean sameFile(URL u1, URL u2) { + if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { + return false; + } + int separator1 = u1.getFile().indexOf(SEPARATOR); + int separator2 = u2.getFile().indexOf(SEPARATOR); + if (separator1 == -1 || separator2 == -1) { + return super.sameFile(u1, u2); + } + String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); + String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); + if (!nested1.equals(nested2)) { + String canonical1 = canonicalize(nested1); + String canonical2 = canonicalize(nested2); + if (!canonical1.equals(canonical2)) { + return false; + } + } + String root1 = u1.getFile().substring(0, separator1); + String root2 = u2.getFile().substring(0, separator2); + try { + return super.sameFile(new URL(root1), new URL(root2)); + } + catch (MalformedURLException ex) { + // Continue + } + return super.sameFile(u1, u2); + } + + private String canonicalize(String path) { + return SEPARATOR_PATTERN.matcher(path).replaceAll("/"); + } + + public JarFile getRootJarFileFromUrl(URL url) throws IOException { + String spec = url.getFile(); + int separatorIndex = spec.indexOf(SEPARATOR); + if (separatorIndex == -1) { + throw new MalformedURLException("Jar URL does not contain !/ separator"); + } + String name = spec.substring(0, separatorIndex); + return getRootJarFile(name); + } + + private JarFile getRootJarFile(String name) throws IOException { + try { + if (!name.startsWith(FILE_PROTOCOL)) { + throw new IllegalStateException("Not a file URL"); + } + File file = new File(URI.create(name)); + Map cache = rootFileCache.get(); + JarFile result = (cache != null) ? cache.get(file) : null; + if (result == null) { + result = new JarFile(file); + addToRootFileCache(file, result); + } + return result; + } + catch (Exception ex) { + throw new IOException("Unable to open root Jar file '" + name + "'", ex); + } + } + + /** + * Add the given {@link JarFile} to the root file cache. + * @param sourceFile the source file to add + * @param jarFile the jar file. + */ + static void addToRootFileCache(File sourceFile, JarFile jarFile) { + Map cache = rootFileCache.get(); + if (cache == null) { + cache = new ConcurrentHashMap<>(); + rootFileCache = new SoftReference<>(cache); + } + cache.put(sourceFile, jarFile); + } + + /** + * If possible, capture a URL that is configured with the original jar handler so that + * we can use it as a fallback context later. We can only do this if we know that we + * can reset the handlers after. + */ + static void captureJarContextUrl() { + if (canResetCachedUrlHandlers()) { + String handlers = System.getProperty(PROTOCOL_HANDLER); + try { + System.clearProperty(PROTOCOL_HANDLER); + try { + resetCachedUrlHandlers(); + jarContextUrl = new URL("jar:file:context.jar!/"); + URLConnection connection = jarContextUrl.openConnection(); + if (connection instanceof JarURLConnection) { + jarContextUrl = null; + } + } + catch (Exception ex) { + } + } + finally { + if (handlers == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, handlers); + } + } + resetCachedUrlHandlers(); + } + } + + private static boolean canResetCachedUrlHandlers() { + try { + resetCachedUrlHandlers(); + return true; + } + catch (Error ex) { + return false; + } + } + + private static void resetCachedUrlHandlers() { + URL.setURLStreamHandlerFactory(null); + } + + /** + * Set if a generic static exception can be thrown when a URL cannot be connected. + * This optimization is used during class loading to save creating lots of exceptions + * which are then swallowed. + * @param useFastConnectionExceptions if fast connection exceptions can be used. + */ + public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) { + JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java new file mode 100644 index 00000000000..8f54dc3070d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarEntry extends java.util.jar.JarEntry implements FileHeader { + + private final int index; + + private final AsciiBytes name; + + private final AsciiBytes headerName; + + private final JarFile jarFile; + + private final long localHeaderOffset; + + private volatile JarEntryCertification certification; + + JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { + super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); + this.index = index; + this.name = (nameAlias != null) ? nameAlias : header.getName(); + this.headerName = header.getName(); + this.jarFile = jarFile; + this.localHeaderOffset = header.getLocalHeaderOffset(); + setCompressedSize(header.getCompressedSize()); + setMethod(header.getMethod()); + setCrc(header.getCrc()); + setComment(header.getComment().toString()); + setSize(header.getSize()); + setTime(header.getTime()); + if (header.hasExtra()) { + setExtra(header.getExtra()); + } + } + + int getIndex() { + return this.index; + } + + AsciiBytes getAsciiBytesName() { + return this.name; + } + + @Override + public boolean hasName(CharSequence name, char suffix) { + return this.headerName.matches(name, suffix); + } + + /** + * Return a {@link URL} for this {@link JarEntry}. + * @return the URL for the entry + * @throws MalformedURLException if the URL is not valid + */ + URL getUrl() throws MalformedURLException { + return new URL(this.jarFile.getUrl(), getName()); + } + + @Override + public Attributes getAttributes() throws IOException { + Manifest manifest = this.jarFile.getManifest(); + return (manifest != null) ? manifest.getAttributes(getName()) : null; + } + + @Override + public Certificate[] getCertificates() { + return getCertification().getCertificates(); + } + + @Override + public CodeSigner[] getCodeSigners() { + return getCertification().getCodeSigners(); + } + + private JarEntryCertification getCertification() { + if (!this.jarFile.isSigned()) { + return JarEntryCertification.NONE; + } + JarEntryCertification certification = this.certification; + if (certification == null) { + certification = this.jarFile.getCertification(this); + this.certification = certification; + } + return certification; + } + + @Override + public long getLocalHeaderOffset() { + return this.localHeaderOffset; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java new file mode 100644 index 00000000000..ffd629e0942 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.security.CodeSigner; +import java.security.cert.Certificate; + +/** + * {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed + * {@link JarFile}. + * + * @author Phillip Webb + */ +class JarEntryCertification { + + static final JarEntryCertification NONE = new JarEntryCertification(null, null); + + private final Certificate[] certificates; + + private final CodeSigner[] codeSigners; + + JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) { + this.certificates = certificates; + this.codeSigners = codeSigners; + } + + Certificate[] getCertificates() { + return (this.certificates != null) ? this.certificates.clone() : null; + } + + CodeSigner[] getCodeSigners() { + return (this.codeSigners != null) ? this.codeSigners.clone() : null; + } + + static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) { + Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null; + CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null; + if (certificates == null && codeSigners == null) { + return NONE; + } + return new JarEntryCertification(certificates, codeSigners); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java new file mode 100644 index 00000000000..6804f0ba37f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +/** + * Interface that can be used to filter and optionally rename jar entries. + * + * @author Phillip Webb + */ +interface JarEntryFilter { + + /** + * Apply the jar entry filter. + * @param name the current entry name. This may be different that the original entry + * name if a previous filter has been applied + * @return the new name of the entry or {@code null} if the entry should not be + * included. + */ + AsciiBytes apply(AsciiBytes name); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java new file mode 100644 index 00000000000..6e548048dbf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -0,0 +1,475 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.security.Permission; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Supplier; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessDataFile; + +/** + * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but + * offers the following additional functionality. + *
    + *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based + * on any directory entry.
  • + *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for + * embedded JAR files (as long as their entry is not compressed).
  • + *
+ * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class JarFile extends AbstractJarFile implements Iterable { + + private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; + + private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); + + private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); + + private static final String READ_ACTION = "read"; + + private final RandomAccessDataFile rootFile; + + private final String pathFromRoot; + + private final RandomAccessData data; + + private final JarFileType type; + + private URL url; + + private String urlString; + + private final JarFileEntries entries; + + private final Supplier manifestSupplier; + + private SoftReference manifest; + + private boolean signed; + + private String comment; + + private volatile boolean closed; + + private volatile JarFileWrapper wrapper; + + /** + * Create a new {@link JarFile} backed by the specified file. + * @param file the root jar file + * @throws IOException if the file cannot be read + */ + public JarFile(File file) throws IOException { + this(new RandomAccessDataFile(file)); + } + + /** + * Create a new {@link JarFile} backed by the specified file. + * @param file the root jar file + * @throws IOException if the file cannot be read + */ + JarFile(RandomAccessDataFile file) throws IOException { + this(file, "", file, JarFileType.DIRECT); + } + + /** + * Private constructor used to create a new {@link JarFile} either directly or from a + * nested entry. + * @param rootFile the root jar file + * @param pathFromRoot the name of this file + * @param data the underlying data + * @param type the type of the jar file + * @throws IOException if the file cannot be read + */ + private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type) + throws IOException { + this(rootFile, pathFromRoot, data, null, type, null); + } + + private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter, + JarFileType type, Supplier manifestSupplier) throws IOException { + super(rootFile.getFile()); + super.close(); + this.rootFile = rootFile; + this.pathFromRoot = pathFromRoot; + CentralDirectoryParser parser = new CentralDirectoryParser(); + this.entries = parser.addVisitor(new JarFileEntries(this, filter)); + this.type = type; + parser.addVisitor(centralDirectoryVisitor()); + try { + this.data = parser.parse(data, filter == null); + } + catch (RuntimeException ex) { + try { + this.rootFile.close(); + super.close(); + } + catch (IOException ioex) { + } + throw ex; + } + this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> { + try (InputStream inputStream = getInputStream(MANIFEST_NAME)) { + if (inputStream == null) { + return null; + } + return new Manifest(inputStream); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }; + } + + private CentralDirectoryVisitor centralDirectoryVisitor() { + return new CentralDirectoryVisitor() { + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + JarFile.this.comment = endRecord.getComment(); + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + AsciiBytes name = fileHeader.getName(); + if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) { + JarFile.this.signed = true; + } + } + + @Override + public void visitEnd() { + } + + }; + } + + JarFileWrapper getWrapper() throws IOException { + JarFileWrapper wrapper = this.wrapper; + if (wrapper == null) { + wrapper = new JarFileWrapper(this); + this.wrapper = wrapper; + } + return wrapper; + } + + @Override + Permission getPermission() { + return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION); + } + + protected final RandomAccessDataFile getRootJarFile() { + return this.rootFile; + } + + RandomAccessData getData() { + return this.data; + } + + @Override + public Manifest getManifest() throws IOException { + Manifest manifest = (this.manifest != null) ? this.manifest.get() : null; + if (manifest == null) { + try { + manifest = this.manifestSupplier.get(); + } + catch (RuntimeException ex) { + throw new IOException(ex); + } + this.manifest = new SoftReference<>(manifest); + } + return manifest; + } + + @Override + public Enumeration entries() { + return new JarEntryEnumeration(this.entries.iterator()); + } + + @Override + public Stream stream() { + Spliterator spliterator = Spliterators.spliterator(iterator(), size(), + Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false); + } + + /** + * Return an iterator for the contained entries. + * @since 2.3.0 + * @see java.lang.Iterable#iterator() + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Iterator iterator() { + return (Iterator) this.entries.iterator(this::ensureOpen); + } + + public JarEntry getJarEntry(CharSequence name) { + return this.entries.getEntry(name); + } + + @Override + public JarEntry getJarEntry(String name) { + return (JarEntry) getEntry(name); + } + + public boolean containsEntry(String name) { + return this.entries.containsEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + ensureOpen(); + return this.entries.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + return this.data.getInputStream(); + } + + @Override + public synchronized InputStream getInputStream(ZipEntry entry) throws IOException { + ensureOpen(); + if (entry instanceof JarEntry jarEntry) { + return this.entries.getInputStream(jarEntry); + } + return getInputStream((entry != null) ? entry.getName() : null); + } + + InputStream getInputStream(String name) throws IOException { + return this.entries.getInputStream(name); + } + + /** + * Return a nested {@link JarFile} loaded from the specified entry. + * @param entry the zip entry + * @return a {@link JarFile} for the entry + * @throws IOException if the nested jar file cannot be read + */ + public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException { + return getNestedJarFile((JarEntry) entry); + } + + /** + * Return a nested {@link JarFile} loaded from the specified entry. + * @param entry the zip entry + * @return a {@link JarFile} for the entry + * @throws IOException if the nested jar file cannot be read + */ + public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException { + try { + return createJarFileFromEntry(entry); + } + catch (Exception ex) { + throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex); + } + } + + private JarFile createJarFileFromEntry(JarEntry entry) throws IOException { + if (entry.isDirectory()) { + return createJarFileFromDirectoryEntry(entry); + } + return createJarFileFromFileEntry(entry); + } + + private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { + AsciiBytes name = entry.getAsciiBytesName(); + JarEntryFilter filter = (candidate) -> { + if (candidate.startsWith(name) && !candidate.equals(name)) { + return candidate.substring(name.length()); + } + return null; + }; + return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1), + this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier); + } + + private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { + if (entry.getMethod() != ZipEntry.STORED) { + throw new IllegalStateException( + "Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested " + + "jar files must be stored without compression. Please check the " + + "mechanism used to create your executable jar file"); + } + RandomAccessData entryData = this.entries.getEntryData(entry.getName()); + return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData, + JarFileType.NESTED_JAR); + } + + @Override + public String getComment() { + ensureOpen(); + return this.comment; + } + + @Override + public int size() { + ensureOpen(); + return this.entries.getSize(); + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + super.close(); + if (this.type == JarFileType.DIRECT) { + this.rootFile.close(); + } + this.closed = true; + } + + private void ensureOpen() { + if (this.closed) { + throw new IllegalStateException("zip file closed"); + } + } + + boolean isClosed() { + return this.closed; + } + + String getUrlString() throws MalformedURLException { + if (this.urlString == null) { + this.urlString = getUrl().toString(); + } + return this.urlString; + } + + @Override + public URL getUrl() throws MalformedURLException { + if (this.url == null) { + String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; + file = file.replace("file:////", "file://"); // Fix UNC paths + this.url = new URL("jar", "", -1, file, new Handler(this)); + } + return this.url; + } + + @Override + public String toString() { + return getName(); + } + + @Override + public String getName() { + return this.rootFile.getFile() + this.pathFromRoot; + } + + boolean isSigned() { + return this.signed; + } + + JarEntryCertification getCertification(JarEntry entry) { + try { + return this.entries.getCertification(entry); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + public void clearCache() { + this.entries.clearCache(); + } + + protected String getPathFromRoot() { + return this.pathFromRoot; + } + + @Override + JarFileType getType() { + return this.type; + } + + /** + * Register a {@literal 'java.protocol.handler.pkgs'} property so that a + * {@link URLStreamHandler} will be located to deal with jar URLs. + */ + public static void registerUrlProtocolHandler() { + Handler.captureJarContextUrl(); + String handlers = System.getProperty(PROTOCOL_HANDLER, ""); + System.setProperty(PROTOCOL_HANDLER, + ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); + resetCachedUrlHandlers(); + } + + /** + * Reset any cached handlers just in case a jar protocol has already been used. We + * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which + * should have no effect other than clearing the handlers cache. + */ + private static void resetCachedUrlHandlers() { + try { + URL.setURLStreamHandlerFactory(null); + } + catch (Error ex) { + // Ignore + } + } + + /** + * An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}. + */ + private static class JarEntryEnumeration implements Enumeration { + + private final Iterator iterator; + + JarEntryEnumeration(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + @Override + public java.util.jar.JarEntry nextElement() { + return this.iterator.next(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java new file mode 100644 index 00000000000..d151c8d80a8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -0,0 +1,491 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Provides access to entries from a {@link JarFile}. In order to reduce memory + * consumption entry details are stored using arrays. The {@code hashCodes} array stores + * the hash code of the entry name, the {@code centralDirectoryOffsets} provides the + * offset to the central directory record and {@code positions} provides the original + * order position of the entry. The arrays are stored in hashCode order so that a binary + * search can be used to find a name. + *

+ * A typical Spring Boot application will have somewhere in the region of 10,500 entries + * which should consume about 122K. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarFileEntries implements CentralDirectoryVisitor, Iterable { + + private static final Runnable NO_VALIDATION = () -> { + }; + + private static final String META_INF_PREFIX = "META-INF/"; + + private static final Name MULTI_RELEASE = new Name("Multi-Release"); + + private static final int BASE_VERSION = 8; + + private static final int RUNTIME_VERSION = Runtime.version().feature(); + + private static final long LOCAL_FILE_HEADER_SIZE = 30; + + private static final char SLASH = '/'; + + private static final char NO_SUFFIX = 0; + + protected static final int ENTRY_CACHE_SIZE = 25; + + private final JarFile jarFile; + + private final JarEntryFilter filter; + + private RandomAccessData centralDirectoryData; + + private int size; + + private int[] hashCodes; + + private Offsets centralDirectoryOffsets; + + private int[] positions; + + private Boolean multiReleaseJar; + + private JarEntryCertification[] certifications; + + private final Map entriesCache = Collections + .synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= ENTRY_CACHE_SIZE; + } + + }); + + JarFileEntries(JarFile jarFile, JarEntryFilter filter) { + this.jarFile = jarFile; + this.filter = filter; + } + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + int maxSize = endRecord.getNumberOfRecords(); + this.centralDirectoryData = centralDirectoryData; + this.hashCodes = new int[maxSize]; + this.centralDirectoryOffsets = Offsets.from(endRecord); + this.positions = new int[maxSize]; + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + AsciiBytes name = applyFilter(fileHeader.getName()); + if (name != null) { + add(name, dataOffset); + } + } + + private void add(AsciiBytes name, long dataOffset) { + this.hashCodes[this.size] = name.hashCode(); + this.centralDirectoryOffsets.set(this.size, dataOffset); + this.positions[this.size] = this.size; + this.size++; + } + + @Override + public void visitEnd() { + sort(0, this.size - 1); + int[] positions = this.positions; + this.positions = new int[positions.length]; + for (int i = 0; i < this.size; i++) { + this.positions[positions[i]] = i; + } + } + + int getSize() { + return this.size; + } + + private void sort(int left, int right) { + // Quick sort algorithm, uses hashCodes as the source but sorts all arrays + if (left < right) { + int pivot = this.hashCodes[left + (right - left) / 2]; + int i = left; + int j = right; + while (i <= j) { + while (this.hashCodes[i] < pivot) { + i++; + } + while (this.hashCodes[j] > pivot) { + j--; + } + if (i <= j) { + swap(i, j); + i++; + j--; + } + } + if (left < j) { + sort(left, j); + } + if (right > i) { + sort(i, right); + } + } + } + + private void swap(int i, int j) { + swap(this.hashCodes, i, j); + this.centralDirectoryOffsets.swap(i, j); + swap(this.positions, i, j); + } + + @Override + public Iterator iterator() { + return new EntryIterator(NO_VALIDATION); + } + + Iterator iterator(Runnable validator) { + return new EntryIterator(validator); + } + + boolean containsEntry(CharSequence name) { + return getEntry(name, FileHeader.class, true) != null; + } + + JarEntry getEntry(CharSequence name) { + return getEntry(name, JarEntry.class, true); + } + + InputStream getInputStream(String name) throws IOException { + FileHeader entry = getEntry(name, FileHeader.class, false); + return getInputStream(entry); + } + + InputStream getInputStream(FileHeader entry) throws IOException { + if (entry == null) { + return null; + } + InputStream inputStream = getEntryData(entry).getInputStream(); + if (entry.getMethod() == ZipEntry.DEFLATED) { + inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize()); + } + return inputStream; + } + + RandomAccessData getEntryData(String name) throws IOException { + FileHeader entry = getEntry(name, FileHeader.class, false); + if (entry == null) { + return null; + } + return getEntryData(entry); + } + + private RandomAccessData getEntryData(FileHeader entry) throws IOException { + // aspectjrt-1.7.4.jar has a different ext bytes length in the + // local directory to the central directory. We need to re-read + // here to skip them + RandomAccessData data = this.jarFile.getData(); + byte[] localHeader = data.read(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE); + long nameLength = Bytes.littleEndianValue(localHeader, 26, 2); + long extraLength = Bytes.littleEndianValue(localHeader, 28, 2); + return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength, + entry.getCompressedSize()); + } + + private T getEntry(CharSequence name, Class type, boolean cacheEntry) { + T entry = doGetEntry(name, type, cacheEntry, null); + if (!isMetaInfEntry(name) && isMultiReleaseJar()) { + int version = RUNTIME_VERSION; + AsciiBytes nameAlias = (entry instanceof JarEntry jarEntry) ? jarEntry.getAsciiBytesName() + : new AsciiBytes(name.toString()); + while (version > BASE_VERSION) { + T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name, type, cacheEntry, nameAlias); + if (versionedEntry != null) { + return versionedEntry; + } + version--; + } + } + return entry; + } + + private boolean isMetaInfEntry(CharSequence name) { + return name.toString().startsWith(META_INF_PREFIX); + } + + private boolean isMultiReleaseJar() { + Boolean multiRelease = this.multiReleaseJar; + if (multiRelease != null) { + return multiRelease; + } + try { + Manifest manifest = this.jarFile.getManifest(); + if (manifest == null) { + multiRelease = false; + } + else { + Attributes attributes = manifest.getMainAttributes(); + multiRelease = attributes.containsKey(MULTI_RELEASE); + } + } + catch (IOException ex) { + multiRelease = false; + } + this.multiReleaseJar = multiRelease; + return multiRelease; + } + + private T doGetEntry(CharSequence name, Class type, boolean cacheEntry, + AsciiBytes nameAlias) { + int hashCode = AsciiBytes.hashCode(name); + T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias); + if (entry == null) { + hashCode = AsciiBytes.hashCode(hashCode, SLASH); + entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias); + } + return entry; + } + + private T getEntry(int hashCode, CharSequence name, char suffix, Class type, + boolean cacheEntry, AsciiBytes nameAlias) { + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + T entry = getEntry(index, type, cacheEntry, nameAlias); + if (entry.hasName(name, suffix)) { + return entry; + } + index++; + } + return null; + } + + @SuppressWarnings("unchecked") + private T getEntry(int index, Class type, boolean cacheEntry, AsciiBytes nameAlias) { + try { + long offset = this.centralDirectoryOffsets.get(index); + FileHeader cached = this.entriesCache.get(index); + FileHeader entry = (cached != null) ? cached + : CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter); + if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { + entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); + } + if (cacheEntry && cached != entry) { + this.entriesCache.put(index, entry); + } + return (T) entry; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private int getFirstIndex(int hashCode) { + int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode); + if (index < 0) { + return -1; + } + while (index > 0 && this.hashCodes[index - 1] == hashCode) { + index--; + } + return index; + } + + void clearCache() { + this.entriesCache.clear(); + } + + private AsciiBytes applyFilter(AsciiBytes name) { + return (this.filter != null) ? this.filter.apply(name) : name; + } + + JarEntryCertification getCertification(JarEntry entry) throws IOException { + JarEntryCertification[] certifications = this.certifications; + if (certifications == null) { + certifications = new JarEntryCertification[this.size]; + // We fall back to use JarInputStream to obtain the certs. This isn't that + // fast, but hopefully doesn't happen too often. + try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { + java.util.jar.JarEntry certifiedEntry; + while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { + // Entry must be closed to trigger a read and set entry certificates + certifiedJarStream.closeEntry(); + int index = getEntryIndex(certifiedEntry.getName()); + if (index != -1) { + certifications[index] = JarEntryCertification.from(certifiedEntry); + } + } + } + this.certifications = certifications; + } + JarEntryCertification certification = certifications[entry.getIndex()]; + return (certification != null) ? certification : JarEntryCertification.NONE; + } + + private int getEntryIndex(CharSequence name) { + int hashCode = AsciiBytes.hashCode(name); + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + FileHeader candidate = getEntry(index, FileHeader.class, false, null); + if (candidate.hasName(name, NO_SUFFIX)) { + return index; + } + index++; + } + return -1; + } + + private static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + private static void swap(long[] array, int i, int j) { + long temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + /** + * Iterator for contained entries. + */ + private final class EntryIterator implements Iterator { + + private final Runnable validator; + + private int index = 0; + + private EntryIterator(Runnable validator) { + this.validator = validator; + validator.run(); + } + + @Override + public boolean hasNext() { + this.validator.run(); + return this.index < JarFileEntries.this.size; + } + + @Override + public JarEntry next() { + this.validator.run(); + if (!hasNext()) { + throw new NoSuchElementException(); + } + int entryIndex = JarFileEntries.this.positions[this.index]; + this.index++; + return getEntry(entryIndex, JarEntry.class, false, null); + } + + } + + /** + * Interface to manage offsets to central directory records. Regular zip files are + * backed by an {@code int[]} based implementation, Zip64 files are backed by a + * {@code long[]} and will consume more memory. + */ + private interface Offsets { + + void set(int index, long value); + + long get(int index); + + void swap(int i, int j); + + static Offsets from(CentralDirectoryEndRecord endRecord) { + int size = endRecord.getNumberOfRecords(); + return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size); + } + + } + + /** + * {@link Offsets} implementation for regular zip files. + */ + private static final class ZipOffsets implements Offsets { + + private final int[] offsets; + + private ZipOffsets(int size) { + this.offsets = new int[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = (int) value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + + /** + * {@link Offsets} implementation for zip64 files. + */ + private static final class Zip64Offsets implements Offsets { + + private final long[] offsets; + + private Zip64Offsets(int size) { + this.offsets = new long[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java new file mode 100644 index 00000000000..b65358947ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +/** + * A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed + * without closing the original. + * + * @author Phillip Webb + */ +class JarFileWrapper extends AbstractJarFile { + + private final JarFile parent; + + JarFileWrapper(JarFile parent) throws IOException { + super(parent.getRootJarFile().getFile()); + this.parent = parent; + super.close(); + } + + @Override + URL getUrl() throws MalformedURLException { + return this.parent.getUrl(); + } + + @Override + JarFileType getType() { + return this.parent.getType(); + } + + @Override + Permission getPermission() { + return this.parent.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.parent.getManifest(); + } + + @Override + public Enumeration entries() { + return this.parent.entries(); + } + + @Override + public Stream stream() { + return this.parent.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + return this.parent.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + return this.parent.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + return this.parent.getInputStream(); + } + + @Override + public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { + return this.parent.getInputStream(ze); + } + + @Override + public String getComment() { + return this.parent.getComment(); + } + + @Override + public int size() { + return this.parent.size(); + } + + @Override + public String toString() { + return this.parent.toString(); + } + + @Override + public String getName() { + return this.parent.getName(); + } + + static JarFile unwrap(java.util.jar.JarFile jarFile) { + if (jarFile instanceof JarFile file) { + return file; + } + if (jarFile instanceof JarFileWrapper wrapper) { + return unwrap(wrapper.parent); + } + throw new IllegalStateException("Not a JarFile or Wrapper"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java new file mode 100644 index 00000000000..859ae88ab00 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -0,0 +1,393 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.URLStreamHandler; +import java.security.Permission; + +/** + * {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Rostyslav Dudka + */ +final class JarURLConnection extends java.net.JarURLConnection { + + private static final ThreadLocal useFastExceptions = new ThreadLocal<>(); + + private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException( + "Jar file or entry not found"); + + private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException( + FILE_NOT_FOUND_EXCEPTION); + + private static final String SEPARATOR = "!/"; + + private static final URL EMPTY_JAR_URL; + + static { + try { + EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) throws IOException { + // Stub URLStreamHandler to prevent the wrong JAR Handler from being + // Instantiated and cached. + return null; + } + }); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new StringSequence("")); + + private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound(); + + private final AbstractJarFile jarFile; + + private Permission permission; + + private URL jarFileUrl; + + private final JarEntryName jarEntryName; + + private java.util.jar.JarEntry jarEntry; + + private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException { + // What we pass to super is ultimately ignored + super(EMPTY_JAR_URL); + this.url = url; + this.jarFile = jarFile; + this.jarEntryName = jarEntryName; + } + + @Override + public void connect() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (!this.jarEntryName.isEmpty() && this.jarEntry == null) { + this.jarEntry = this.jarFile.getJarEntry(getEntryName()); + if (this.jarEntry == null) { + throwFileNotFound(this.jarEntryName, this.jarFile); + } + } + this.connected = true; + } + + @Override + public java.util.jar.JarFile getJarFile() throws IOException { + connect(); + return this.jarFile; + } + + @Override + public URL getJarFileURL() { + if (this.jarFile == null) { + throw NOT_FOUND_CONNECTION_EXCEPTION; + } + if (this.jarFileUrl == null) { + this.jarFileUrl = buildJarFileUrl(); + } + return this.jarFileUrl; + } + + private URL buildJarFileUrl() { + try { + String spec = this.jarFile.getUrl().getFile(); + if (spec.endsWith(SEPARATOR)) { + spec = spec.substring(0, spec.length() - SEPARATOR.length()); + } + if (!spec.contains(SEPARATOR)) { + return new URL(spec); + } + return new URL("jar:" + spec); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public java.util.jar.JarEntry getJarEntry() throws IOException { + if (this.jarEntryName == null || this.jarEntryName.isEmpty()) { + return null; + } + connect(); + return this.jarEntry; + } + + @Override + public String getEntryName() { + if (this.jarFile == null) { + throw NOT_FOUND_CONNECTION_EXCEPTION; + } + return this.jarEntryName.toString(); + } + + @Override + public InputStream getInputStream() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFile.JarFileType.DIRECT) { + throw new IOException("no entry name specified"); + } + connect(); + InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream() + : this.jarFile.getInputStream(this.jarEntry)); + if (inputStream == null) { + throwFileNotFound(this.jarEntryName, this.jarFile); + } + return inputStream; + } + + private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException { + if (Boolean.TRUE.equals(useFastExceptions.get())) { + throw FILE_NOT_FOUND_EXCEPTION; + } + throw new FileNotFoundException("JAR entry " + entry + " not found in " + jarFile.getName()); + } + + @Override + public int getContentLength() { + long length = getContentLengthLong(); + if (length > Integer.MAX_VALUE) { + return -1; + } + return (int) length; + } + + @Override + public long getContentLengthLong() { + if (this.jarFile == null) { + return -1; + } + try { + if (this.jarEntryName.isEmpty()) { + return this.jarFile.size(); + } + java.util.jar.JarEntry entry = getJarEntry(); + return (entry != null) ? (int) entry.getSize() : -1; + } + catch (IOException ex) { + return -1; + } + } + + @Override + public Object getContent() throws IOException { + connect(); + return this.jarEntryName.isEmpty() ? this.jarFile : super.getContent(); + } + + @Override + public String getContentType() { + return (this.jarEntryName != null) ? this.jarEntryName.getContentType() : null; + } + + @Override + public Permission getPermission() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (this.permission == null) { + this.permission = this.jarFile.getPermission(); + } + return this.permission; + } + + @Override + public long getLastModified() { + if (this.jarFile == null || this.jarEntryName.isEmpty()) { + return 0; + } + try { + java.util.jar.JarEntry entry = getJarEntry(); + return (entry != null) ? entry.getTime() : 0; + } + catch (IOException ex) { + return 0; + } + } + + static void setUseFastExceptions(boolean useFastExceptions) { + JarURLConnection.useFastExceptions.set(useFastExceptions); + } + + static JarURLConnection get(URL url, JarFile jarFile) throws IOException { + StringSequence spec = new StringSequence(url.getFile()); + int index = indexOfRootSpec(spec, jarFile.getPathFromRoot()); + if (index == -1) { + return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION + : new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME)); + } + int separator; + while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { + JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator)); + JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence()); + if (jarEntry == null) { + return JarURLConnection.notFound(jarFile, entryName); + } + jarFile = jarFile.getNestedJarFile(jarEntry); + index = separator + SEPARATOR.length(); + } + JarEntryName jarEntryName = JarEntryName.get(spec, index); + if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty() + && !jarFile.containsEntry(jarEntryName.toString())) { + return NOT_FOUND_CONNECTION; + } + return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName); + } + + private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { + int separatorIndex = file.indexOf(SEPARATOR); + if (separatorIndex < 0 || !file.startsWith(pathFromRoot, separatorIndex)) { + return -1; + } + return separatorIndex + SEPARATOR.length() + pathFromRoot.length(); + } + + private static JarURLConnection notFound() { + try { + return notFound(null, null); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException { + if (Boolean.TRUE.equals(useFastExceptions.get())) { + return NOT_FOUND_CONNECTION; + } + return new JarURLConnection(null, jarFile, jarEntryName); + } + + /** + * A JarEntryName parsed from a URL String. + */ + static class JarEntryName { + + private final StringSequence name; + + private String contentType; + + JarEntryName(StringSequence spec) { + this.name = decode(spec); + } + + private StringSequence decode(StringSequence source) { + if (source.isEmpty() || (source.indexOf('%') < 0)) { + return source; + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); + write(source.toString(), bos); + // AsciiBytes is what is used to store the JarEntries so make it symmetric + return new StringSequence(AsciiBytes.toString(bos.toByteArray())); + } + + private void write(String source, ByteArrayOutputStream outputStream) { + int length = source.length(); + for (int i = 0; i < length; i++) { + int c = source.charAt(i); + if (c > 127) { + try { + String encoded = URLEncoder.encode(String.valueOf((char) c), "UTF-8"); + write(encoded, outputStream); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + else { + if (c == '%') { + if ((i + 2) >= length) { + throw new IllegalArgumentException( + "Invalid encoded sequence \"" + source.substring(i) + "\""); + } + c = decodeEscapeSequence(source, i); + i += 2; + } + outputStream.write(c); + } + } + } + + private char decodeEscapeSequence(String source, int i) { + int hi = Character.digit(source.charAt(i + 1), 16); + int lo = Character.digit(source.charAt(i + 2), 16); + if (hi == -1 || lo == -1) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + return ((char) ((hi << 4) + lo)); + } + + CharSequence toCharSequence() { + return this.name; + } + + @Override + public String toString() { + return this.name.toString(); + } + + boolean isEmpty() { + return this.name.isEmpty(); + } + + String getContentType() { + if (this.contentType == null) { + this.contentType = deduceContentType(); + } + return this.contentType; + } + + private String deduceContentType() { + // Guess the content type, don't bother with streams as mark is not supported + String type = isEmpty() ? "x-java/jar" : null; + type = (type != null) ? type : guessContentTypeFromName(toString()); + type = (type != null) ? type : "content/unknown"; + return type; + } + + static JarEntryName get(StringSequence spec) { + return get(spec, 0); + } + + static JarEntryName get(StringSequence spec, int beginIndex) { + if (spec.length() <= beginIndex) { + return EMPTY_JAR_ENTRY_NAME; + } + return new JarEntryName(spec.subSequence(beginIndex)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java new file mode 100644 index 00000000000..12850a4ebe3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.util.Objects; + +/** + * A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular + * {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying + * character array. + * + * @author Phillip Webb + */ +final class StringSequence implements CharSequence { + + private final String source; + + private final int start; + + private final int end; + + private int hash; + + StringSequence(String source) { + this(source, 0, (source != null) ? source.length() : -1); + } + + StringSequence(String source, int start, int end) { + Objects.requireNonNull(source, "Source must not be null"); + if (start < 0) { + throw new StringIndexOutOfBoundsException(start); + } + if (end > source.length()) { + throw new StringIndexOutOfBoundsException(end); + } + this.source = source; + this.start = start; + this.end = end; + } + + StringSequence subSequence(int start) { + return subSequence(start, length()); + } + + @Override + public StringSequence subSequence(int start, int end) { + int subSequenceStart = this.start + start; + int subSequenceEnd = this.start + end; + if (subSequenceStart > this.end) { + throw new StringIndexOutOfBoundsException(start); + } + if (subSequenceEnd > this.end) { + throw new StringIndexOutOfBoundsException(end); + } + if (start == 0 && subSequenceEnd == this.end) { + return this; + } + return new StringSequence(this.source, subSequenceStart, subSequenceEnd); + } + + /** + * Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15. + * @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false} + */ + public boolean isEmpty() { + return length() == 0; + } + + @Override + public int length() { + return this.end - this.start; + } + + @Override + public char charAt(int index) { + return this.source.charAt(this.start + index); + } + + int indexOf(char ch) { + return this.source.indexOf(ch, this.start) - this.start; + } + + int indexOf(String str) { + return this.source.indexOf(str, this.start) - this.start; + } + + int indexOf(String str, int fromIndex) { + return this.source.indexOf(str, this.start + fromIndex) - this.start; + } + + boolean startsWith(String prefix) { + return startsWith(prefix, 0); + } + + boolean startsWith(String prefix, int offset) { + int prefixLength = prefix.length(); + int length = length(); + if (length - prefixLength - offset < 0) { + return false; + } + return this.source.startsWith(prefix, this.start + offset); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CharSequence other)) { + return false; + } + int n = length(); + if (n != other.length()) { + return false; + } + int i = 0; + while (n-- != 0) { + if (charAt(i) != other.charAt(i)) { + return false; + } + i++; + } + return true; + } + + @Override + public int hashCode() { + int hash = this.hash; + if (hash == 0 && length() > 0) { + for (int i = this.start; i < this.end; i++) { + hash = 31 * hash + this.source.charAt(i); + } + this.hash = hash; + } + return hash; + } + + @Override + public String toString() { + return this.source.substring(this.start, this.end); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java new file mode 100644 index 00000000000..67624460ccd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which + * is required with JDK 6) and returns accurate available() results. + * + * @author Phillip Webb + */ +class ZipInflaterInputStream extends InflaterInputStream { + + private int available; + + private boolean extraBytesWritten; + + ZipInflaterInputStream(InputStream inputStream, int size) { + super(inputStream, new Inflater(true), getInflaterBufferSize(size)); + this.available = size; + } + + @Override + public int available() throws IOException { + if (this.available < 0) { + return super.available(); + } + return this.available; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result = super.read(b, off, len); + if (result != -1) { + this.available -= result; + } + return result; + } + + @Override + public void close() throws IOException { + super.close(); + this.inf.end(); + } + + @Override + protected void fill() throws IOException { + try { + super.fill(); + } + catch (EOFException ex) { + if (this.extraBytesWritten) { + throw ex; + } + this.len = 1; + this.buf[0] = 0x0; + this.extraBytesWritten = true; + this.inf.setInput(this.buf, 0, this.len); + } + } + + private static int getInflaterBufferSize(long size) { + size += 2; // inflater likes some space + size = (size > 65536) ? 8192 : size; + size = (size <= 0) ? 4096 : size; + return (int) size; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java new file mode 100644 index 00000000000..638afe45f49 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 and manipulating JAR/WAR files. + */ +package org.springframework.boot.loader.jar; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java new file mode 100644 index 00000000000..162e4a6a739 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jarmode; + +/** + * Interface registered in {@code spring.factories} to provides extended 'jarmode' + * support. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface JarMode { + + /** + * Returns if this accepts and can run the given mode. + * @param mode the mode to check + * @return if this instance accepts the mode + */ + boolean accepts(String mode); + + /** + * Run the jar in the given mode. + * @param mode the mode to use + * @param args any program arguments + */ + void run(String mode, String[] args); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java new file mode 100644 index 00000000000..44fcb7902ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jarmode; + +import java.util.List; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +/** + * Delegate class used to launch the fat jar in a specific mode. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class JarModeLauncher { + + static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + + private JarModeLauncher() { + } + + public static void main(String[] args) { + String mode = System.getProperty("jarmode"); + List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, + ClassUtils.getDefaultClassLoader()); + for (JarMode candidate : candidates) { + if (candidate.accepts(mode)) { + candidate.run(mode, args); + return; + } + } + System.err.println("Unsupported jarmode '" + mode + "'"); + if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { + System.exit(1); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java new file mode 100644 index 00000000000..2e17175690a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jarmode; + +import java.util.Arrays; + +/** + * {@link JarMode} for testing. + * + * @author Phillip Webb + */ +class TestJarMode implements JarMode { + + @Override + public boolean accepts(String mode) { + return "test".equals(mode); + } + + @Override + public void run(String mode, String[] args) { + System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java new file mode 100644 index 00000000000..2f3b5a74e8f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 launching the JAR using jarmode. + * + * @see org.springframework.boot.loader.jarmode.JarModeLauncher + */ +package org.springframework.boot.loader.jarmode; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java new file mode 100644 index 00000000000..5beb8d10964 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.JarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class JarLauncher { + + private JarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.JarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java new file mode 100644 index 00000000000..d80fb0bb710 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class PropertiesLauncher { + + private PropertiesLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.PropertiesLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java new file mode 100644 index 00000000000..9392d3bf2b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.WarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class WarLauncher { + + private WarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.WarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java new file mode 100644 index 00000000000..7968d509a2b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/** + * Repackaged launcher classes. + * + * @see org.springframework.boot.loader.launch.JarLauncher + * @see org.springframework.boot.loader.launch.WarLauncher + */ +package org.springframework.boot.loader.launch; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java new file mode 100644 index 00000000000..4b32f644f54 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/** + * System that allows self-contained JAR/WAR archives to be launched using + * {@code java -jar}. Archives can include nested packaged dependency JARs (there is no + * need to create shade style jars) and are executed without unpacking. The only + * constraint is that nested JARs must be stored in the archive uncompressed. + * + * @see org.springframework.boot.loader.JarLauncher + * @see org.springframework.boot.loader.WarLauncher + */ +package org.springframework.boot.loader; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java new file mode 100644 index 00000000000..df00705e9ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.util; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +/** + * Helper class for resolving placeholders in texts. Usually applied to file paths. + *

+ * A text may contain {@code $ ...} placeholders, to be resolved as system properties: + * e.g. {@code $ user.dir}. Default values can be supplied using the ":" separator between + * key and value. + *

+ * Adapted from Spring. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @since 1.0.0 + * @see System#getProperty(String) + */ +public abstract class SystemPropertyUtils { + + /** + * Prefix for system property placeholders: "${". + */ + public static final String PLACEHOLDER_PREFIX = "${"; + + /** + * Suffix for system property placeholders: "}". + */ + public static final String PLACEHOLDER_SUFFIX = "}"; + + /** + * Value separator for system property placeholders: ":". + */ + public static final String VALUE_SEPARATOR = ":"; + + private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1); + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * system property values. + * @param text the String to resolve + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + */ + public static String resolvePlaceholders(String text) { + if (text == null) { + return text; + } + return parseStringValue(null, text, text, new HashSet<>()); + } + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * system property values. + * @param properties a properties instance to use in addition to System + * @param text the String to resolve + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + */ + public static String resolvePlaceholders(Properties properties, String text) { + if (text == null) { + return text; + } + return parseStringValue(properties, text, text, new HashSet<>()); + } + + private static String parseStringValue(Properties properties, String value, String current, + Set visitedPlaceholders) { + + StringBuilder buf = new StringBuilder(current); + + int startIndex = current.indexOf(PLACEHOLDER_PREFIX); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(buf, startIndex); + if (endIndex != -1) { + String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + String originalPlaceholder = placeholder; + if (!visitedPlaceholders.add(originalPlaceholder)) { + throw new IllegalArgumentException( + "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); + } + // Recursive invocation, parsing placeholders contained in the + // placeholder + // key. + placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders); + // Now obtain the value for the fully resolved key... + String propVal = resolvePlaceholder(properties, value, placeholder); + if (propVal == null) { + int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR); + if (separatorIndex != -1) { + String actualPlaceholder = placeholder.substring(0, separatorIndex); + String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length()); + propVal = resolvePlaceholder(properties, value, actualPlaceholder); + if (propVal == null) { + propVal = defaultValue; + } + } + } + if (propVal != null) { + // Recursive invocation, parsing placeholders contained in the + // previously resolved placeholder value. + propVal = parseStringValue(properties, value, propVal, visitedPlaceholders); + buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal); + startIndex = buf.indexOf(PLACEHOLDER_PREFIX, startIndex + propVal.length()); + } + else { + // Proceed with unprocessed value. + startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length()); + } + visitedPlaceholders.remove(originalPlaceholder); + } + else { + startIndex = -1; + } + } + + return buf.toString(); + } + + private static String resolvePlaceholder(Properties properties, String text, String placeholderName) { + String propVal = getProperty(placeholderName, null, text); + if (propVal != null) { + return propVal; + } + return (properties != null) ? properties.getProperty(placeholderName) : null; + } + + public static String getProperty(String key) { + return getProperty(key, null, ""); + } + + public static String getProperty(String key, String defaultValue) { + return getProperty(key, defaultValue, ""); + } + + /** + * Search the System properties and environment variables for a value with the + * provided key. Environment variables in {@code UPPER_CASE} style are allowed where + * System properties would normally be {@code lower.case}. + * @param key the key to resolve + * @param defaultValue the default value + * @param text optional extra context for an error message if the key resolution fails + * (e.g. if System properties are not accessible) + * @return a static property value or null of not found + */ + public static String getProperty(String key, String defaultValue, String text) { + try { + String propVal = System.getProperty(key); + if (propVal == null) { + // Fall back to searching the system environment. + propVal = System.getenv(key); + } + if (propVal == null) { + // Try with underscores. + String name = key.replace('.', '_'); + propVal = System.getenv(name); + } + if (propVal == null) { + // Try uppercase with underscores as well. + String name = key.toUpperCase(Locale.ENGLISH).replace('.', '_'); + propVal = System.getenv(name); + } + if (propVal != null) { + return propVal; + } + } + catch (Throwable ex) { + System.err.println("Could not resolve key '" + key + "' in '" + text + + "' as system property or in environment: " + ex); + } + return defaultValue; + } + + private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } + else { + return index; + } + } + else if (substringMatch(buf, index, SIMPLE_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PREFIX.length(); + } + else { + index++; + } + } + return -1; + } + + private static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java new file mode 100644 index 00000000000..d3d7eef2d9d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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. + */ + +/** + * Utilities used by Spring Boot's JAR loading. + */ +package org.springframework.boot.loader.util; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java new file mode 100644 index 00000000000..60e3cb2765e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.util.FileCopyUtils; + +/** + * Base class for testing {@link ExecutableArchiveLauncher} implementations. + * + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + */ +public abstract class AbstractExecutableArchiveLauncherTests { + + @TempDir + File tempDir; + + protected File createJarArchive(String name, String entryPrefix) throws IOException { + return createJarArchive(name, entryPrefix, false, Collections.emptyList()); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, String entryPrefix, boolean indexed, List extraLibs) + throws IOException { + return createJarArchive(name, null, entryPrefix, indexed, extraLibs); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed, + List extraLibs) throws IOException { + File archive = new File(this.tempDir, name); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(archive)); + if (manifest != null) { + jarOutputStream.putNextEntry(new JarEntry("META-INF/")); + jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/")); + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/")); + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/")); + if (indexed) { + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classpath.idx")); + Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); + writer.write("- \"" + entryPrefix + "/lib/foo.jar\"\n"); + writer.write("- \"" + entryPrefix + "/lib/bar.jar\"\n"); + writer.write("- \"" + entryPrefix + "/lib/baz.jar\"\n"); + writer.flush(); + jarOutputStream.closeEntry(); + } + addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream); + for (String lib : extraLibs) { + addNestedJars(entryPrefix, "/lib/" + lib, jarOutputStream); + } + jarOutputStream.close(); + return archive; + } + + private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException { + JarEntry libFoo = new JarEntry(entryPrefix + lib); + libFoo.setMethod(ZipEntry.STORED); + ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream(); + new JarOutputStream(fooJarStream).close(); + libFoo.setSize(fooJarStream.size()); + CRC32 crc32 = new CRC32(); + crc32.update(fooJarStream.toByteArray()); + libFoo.setCrc(crc32.getValue()); + jarOutputStream.putNextEntry(libFoo); + jarOutputStream.write(fooJarStream.toByteArray()); + } + + protected File explode(File archive) throws IOException { + File exploded = new File(this.tempDir, "exploded"); + exploded.mkdirs(); + JarFile jarFile = new JarFile(archive); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File entryFile = new File(exploded, entry.getName()); + if (entry.isDirectory()) { + entryFile.mkdirs(); + } + else { + FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(entryFile)); + } + } + jarFile.close(); + return exploded; + } + + protected Set getUrls(List archives) throws MalformedURLException { + Set urls = new LinkedHashSet<>(archives.size()); + for (Archive archive : archives) { + urls.add(archive.getUrl()); + } + return urls; + } + + protected final URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java new file mode 100644 index 00000000000..4cd1b4e8d28 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +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.assertThatIllegalArgumentException; + +/** + * Tests for {@link ClassPathIndexFile}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ClassPathIndexFileTests { + + @TempDir + File temp; + + @Test + void loadIfPossibleWhenRootIsNotFileReturnsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx")) + .withMessage("URL does not reference a file"); + } + + @Test + void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception { + File root = new File(this.temp, "missing"); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception { + File root = new File(this.temp, "directory"); + root.mkdirs(); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleReturnsInstance() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile).isNotNull(); + } + + @Test + void sizeReturnsNumberOfLines() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.size()).isEqualTo(5); + } + + @Test + void getUrlsReturnsUrls() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + List urls = indexFile.getUrls(); + List expected = new ArrayList<>(); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/a.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/b.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/c.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/d.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/e.jar")); + assertThat(urls).containsExactly(expected.stream().map(this::toUrl).toArray(URL[]::new)); + } + + private URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException { + copyTestIndexFile(); + ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx"); + return indexFile; + } + + private void copyTestIndexFile() throws IOException { + Files.copy(getClass().getResourceAsStream("classpath-index-file.idx"), + new File(this.temp, "test.idx").toPath()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java new file mode 100644 index 00000000000..afa32a7c4f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.function.ThrowingConsumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarLauncher}. + * + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { + + @Test + void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF")); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); + for (Archive archive : archives) { + archive.close(); + } + } + + @Test + void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { + File jarRoot = createJarArchive("archive.jar", "BOOT-INF"); + try (JarFileArchive archive = new JarFileArchive(jarRoot)) { + JarLauncher launcher = new JarLauncher(archive); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); + assertThat(classPathArchives).hasSize(4); + assertThat(getUrls(classPathArchives)).containsOnly( + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/")); + for (Archive classPathArchive : classPathArchives) { + classPathArchive.close(); + } + } + } + + @Test + void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList())); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + + @Test + void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception { + ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); + URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactly(expectedFileUrls); + } + + @Test + void explodedJarDefinedPackagesIncludeManifestAttributes() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Name.MANIFEST_VERSION, "1.0"); + attributes.put(Name.IMPLEMENTATION_TITLE, "test"); + SourceFile sourceFile = SourceFile.of("explodedsample/ExampleClass.java", + new ClassPathResource("explodedsample/ExampleClass.txt")); + TestCompiler.forSystem().compile(sourceFile, ThrowingConsumer.of((compiled) -> { + File explodedRoot = explode( + createJarArchive("archive.jar", manifest, "BOOT-INF", true, Collections.emptyList())); + File target = new File(explodedRoot, "BOOT-INF/classes/explodedsample/ExampleClass.class"); + target.getParentFile().mkdirs(); + FileCopyUtils.copy(compiled.getClassLoader().getResourceAsStream("explodedsample/ExampleClass.class"), + new FileOutputStream(target)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + Class loaded = classLoader.loadClass("explodedsample.ExampleClass"); + assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test"); + })); + } + + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + + protected final List getExpectedFilesWithExtraLibs(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar")); + expected.add(new File(parent, "BOOT-INF/lib/extra-2.jar")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java new file mode 100644 index 00000000000..58084bba8ab --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.jar.JarFile; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LaunchedURLClassLoader}. + * + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + */ +@SuppressWarnings("resource") +class LaunchedURLClassLoaderTests { + + @TempDir + File tempDir; + + @Test + void resolveResourceFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResource("demo/Application.java")).isNotNull(); + } + + @Test + void resolveResourcesFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue(); + } + + @Test + void resolveRootPathFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResource("")).isNotNull(); + } + + @Test + void resolveRootResourcesFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResources("").hasMoreElements()).isTrue(); + } + + @Test + void resolveFromNested() throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + try (JarFile jarFile = new JarFile(file)) { + URL url = jarFile.getUrl(); + try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { + URL resource = loader.getResource("nested.jar!/3.dat"); + assertThat(resource).hasToString(url + "nested.jar!/3.dat"); + try (InputStream input = resource.openConnection().getInputStream()) { + assertThat(input.read()).isEqualTo(3); + } + } + } + } + + @Test + void resolveFromNestedWhileThreadIsInterrupted() throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + try (JarFile jarFile = new JarFile(file)) { + URL url = jarFile.getUrl(); + try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { + Thread.currentThread().interrupt(); + URL resource = loader.getResource("nested.jar!/3.dat"); + assertThat(resource).hasToString(url + "nested.jar!/3.dat"); + URLConnection connection = resource.openConnection(); + try (InputStream input = connection.getInputStream()) { + assertThat(input.read()).isEqualTo(3); + } + ((JarURLConnection) connection).getJarFile().close(); + } + finally { + Thread.interrupted(); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java new file mode 100644 index 00000000000..ab7c296b38b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.assertj.core.api.Condition; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.jar.Handler; +import org.springframework.boot.loader.jar.JarFile; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.core.io.FileSystemResource; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.hamcrest.Matchers.containsString; + +/** + * Tests for {@link PropertiesLauncher}. + * + * @author Dave Syer + * @author Andy Wilkinson + */ +@ExtendWith(OutputCaptureExtension.class) +class PropertiesLauncherTests { + + @TempDir + File tempDir; + + private PropertiesLauncher launcher; + + private ClassLoader contextClassLoader; + + private CapturedOutput output; + + @BeforeEach + void setup(CapturedOutput capturedOutput) throws Exception { + this.contextClassLoader = Thread.currentThread().getContextClassLoader(); + clearHandlerCache(); + System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath()); + this.output = capturedOutput; + } + + @AfterEach + void close() throws Exception { + Thread.currentThread().setContextClassLoader(this.contextClassLoader); + System.clearProperty("loader.home"); + System.clearProperty("loader.path"); + System.clearProperty("loader.main"); + System.clearProperty("loader.config.name"); + System.clearProperty("loader.config.location"); + System.clearProperty("loader.system"); + System.clearProperty("loader.classLoader"); + clearHandlerCache(); + if (this.launcher != null) { + this.launcher.close(); + } + } + + @SuppressWarnings("unchecked") + private void clearHandlerCache() throws Exception { + Map rootFileCache = ((SoftReference>) ReflectionTestUtils + .getField(Handler.class, "rootFileCache")).get(); + if (rootFileCache != null) { + for (JarFile rootJarFile : rootFileCache.values()) { + rootJarFile.close(); + } + rootFileCache.clear(); + } + } + + @Test + void testDefaultHome() { + System.clearProperty("loader.home"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir"))); + } + + @Test + void testAlternateHome() throws Exception { + System.setProperty("loader.home", "src/test/resources/home"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("loader.home"))); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.HomeApplication"); + } + + @Test + void testNonExistentHome() { + System.setProperty("loader.home", "src/test/resources/nonexistent"); + assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new) + .withMessageContaining("Invalid source directory") + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void testUserSpecifiedMain() throws Exception { + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.Application"); + assertThat(System.getProperty("loader.main")).isNull(); + } + + @Test + void testUserSpecifiedConfigName() throws Exception { + System.setProperty("loader.config.name", "foo"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.Application"); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[etc/]"); + } + + @Test + void testRootOfClasspathFirst() throws Exception { + System.setProperty("loader.config.name", "bar"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); + } + + @Test + void testUserSpecifiedDotPath() { + System.setProperty("loader.path", "."); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[.]"); + } + + @Test + void testUserSpecifiedSlashPath() throws Exception { + System.setProperty("loader.path", "jars/"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]"); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedWildcardPath() throws Exception { + System.setProperty("loader.path", "jars/*"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedJarPath() throws Exception { + System.setProperty("loader.path", "jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedRootOfJarPath() throws Exception { + System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedRootOfJarPathWithDot() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar!/./"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { + System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + } + + @Test + void testUserSpecifiedJarFileWithNestedArchives() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedNestedJarPath() throws Exception { + System.setProperty("loader.path", "nested-jars/nested-jar-app.jar!/BOOT-INF/classes/"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[nested-jars/nested-jar-app.jar!/BOOT-INF/classes/]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives() throws Exception { + System.setProperty("loader.path", "nested-jars"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedJarPathWithDot() throws Exception { + System.setProperty("loader.path", "./jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedClassLoader() throws Exception { + System.setProperty("loader.path", "jars/app.jar"); + System.setProperty("loader.classLoader", URLClassLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedClassPathOrder() throws Exception { + System.setProperty("loader.path", "more-jars/app.jar,jars/app.jar"); + System.setProperty("loader.classLoader", URLClassLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[more-jars/app.jar, jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello Other World"); + } + + @Test + void testCustomClassLoaderCreation() throws Exception { + System.setProperty("loader.classLoader", TestLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + ClassLoader loader = this.launcher.createClassLoader(archives()); + assertThat(loader).isNotNull(); + assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); + } + + private Iterator archives() throws Exception { + List archives = new ArrayList<>(); + String path = System.getProperty("java.class.path"); + for (String url : path.split(File.pathSeparator)) { + Archive archive = archive(url); + if (archive != null) { + archives.add(archive); + } + } + return archives.iterator(); + } + + private Archive archive(String url) throws IOException { + File file = new FileSystemResource(url).getFile(); + if (!file.exists()) { + return null; + } + if (url.endsWith(".jar")) { + return new JarFileArchive(file); + } + return new ExplodedArchive(file); + } + + @Test + void testUserSpecifiedConfigPathWins() throws Exception { + System.setProperty("loader.config.name", "foo"); + System.setProperty("loader.config.location", "classpath:bar.properties"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); + } + + @Test + void testSystemPropertySpecifiedMain() throws Exception { + System.setProperty("loader.main", "foo.Bar"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("foo.Bar"); + } + + @Test + void testSystemPropertiesSet() { + System.setProperty("loader.system", "true"); + new PropertiesLauncher(); + assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application"); + } + + @Test + void testArgsEnhanced() throws Exception { + System.setProperty("loader.args", "foo"); + this.launcher = new PropertiesLauncher(); + assertThat(Arrays.asList(this.launcher.getArgs("bar"))).hasToString("[foo, bar]"); + } + + @SuppressWarnings("unchecked") + @Test + void testLoadPathCustomizedUsingManifest() throws Exception { + System.setProperty("loader.home", this.tempDir.getAbsolutePath()); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Loader-Path", "/foo.jar, /bar"); + File manifestFile = new File(this.tempDir, "META-INF/MANIFEST.MF"); + manifestFile.getParentFile().mkdirs(); + try (FileOutputStream manifestStream = new FileOutputStream(manifestFile)) { + manifest.write(manifestStream); + } + this.launcher = new PropertiesLauncher(); + assertThat((List) ReflectionTestUtils.getField(this.launcher, "paths")).containsExactly("/foo.jar", + "/bar/"); + } + + @Test + void testManifestWithPlaceholders() throws Exception { + System.setProperty("loader.home", "src/test/resources/placeholders"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.FooApplication"); + } + + @Test + void encodedFileUrlLoaderPathIsHandledCorrectly() throws Exception { + File loaderPath = new File(this.tempDir, "loader path"); + loaderPath.mkdir(); + System.setProperty("loader.path", loaderPath.toURI().toURL().toString()); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).hasSize(1); + File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root"); + assertThat(archiveRoot).isEqualTo(loaderPath); + } + + @Test // gh-21575 + void loadResourceFromJarFile() throws Exception { + File jarFile = new File(this.tempDir, "app.jar"); + TestJarCreator.createTestJar(jarFile); + System.setProperty("loader.home", this.tempDir.getAbsolutePath()); + System.setProperty("loader.path", "app.jar"); + this.launcher = new PropertiesLauncher(); + try { + this.launcher.launch(new String[0]); + } + catch (Exception ex) { + // Expected ClassNotFoundException + LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread() + .getContextClassLoader(); + classLoader.close(); + } + URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat"); + byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream()); + assertThat(bytes).isNotEmpty(); + } + + private void waitFor(String value) { + Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value)); + } + + private Condition endingWith(String value) { + return new Condition<>() { + + @Override + public boolean matches(Archive archive) { + return archive.toString().endsWith(value); + } + + }; + } + + static class TestLoader extends URLClassLoader { + + TestLoader(ClassLoader parent) { + super(new URL[0], parent); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java new file mode 100644 index 00000000000..c5c5fd3b95c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +/** + * Creates a simple test jar. + * + * @author Phillip Webb + */ +public abstract class TestJarCreator { + + private static final int BASE_VERSION = 8; + + private static final int RUNTIME_VERSION; + + static { + int version; + try { + Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); + version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion); + } + catch (Throwable ex) { + version = BASE_VERSION; + } + RUNTIME_VERSION = version; + } + + public static void createTestJar(File file) throws Exception { + createTestJar(file, false); + } + + public static void createTestJar(File file, boolean unpackNested) throws Exception { + FileOutputStream fileOutputStream = new FileOutputStream(file); + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment("outer"); + writeManifest(jarOutputStream, "j1"); + writeEntry(jarOutputStream, "1.dat", 1); + writeEntry(jarOutputStream, "2.dat", 2); + writeDirEntry(jarOutputStream, "d/"); + writeEntry(jarOutputStream, "d/9.dat", 9); + writeDirEntry(jarOutputStream, "special/"); + writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB'); + writeNestedEntry("nested.jar", unpackNested, jarOutputStream); + writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream); + writeNestedEntry("space nested.jar", unpackNested, jarOutputStream); + writeNestedMultiReleaseEntry("multi-release.jar", unpackNested, jarOutputStream); + } + } + + private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream) + throws Exception { + writeNestedEntry(name, unpackNested, jarOutputStream, false); + } + + private static void writeNestedMultiReleaseEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream) + throws Exception { + writeNestedEntry(name, unpackNested, jarOutputStream, true); + } + + private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream, + boolean multiRelease) throws Exception { + JarEntry nestedEntry = new JarEntry(name); + byte[] nestedJarData = getNestedJarData(multiRelease); + nestedEntry.setSize(nestedJarData.length); + nestedEntry.setCompressedSize(nestedJarData.length); + if (unpackNested) { + nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000"); + } + CRC32 crc32 = new CRC32(); + crc32.update(nestedJarData); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutputStream.putNextEntry(nestedEntry); + jarOutputStream.write(nestedJarData); + jarOutputStream.closeEntry(); + } + + private static byte[] getNestedJarData(boolean multiRelease) throws Exception { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); + jarOutputStream.setComment("nested"); + writeManifest(jarOutputStream, "j2", multiRelease); + if (multiRelease) { + writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION); + writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", RUNTIME_VERSION), + RUNTIME_VERSION); + } + else { + writeEntry(jarOutputStream, "3.dat", 3); + writeEntry(jarOutputStream, "4.dat", 4); + writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4'); + } + jarOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name) throws Exception { + writeManifest(jarOutputStream, name, false); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name, boolean multiRelease) + throws Exception { + writeDirEntry(jarOutputStream, "META-INF/"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Built-By", name); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + if (multiRelease) { + manifest.getMainAttributes().putValue("Multi-Release", Boolean.toString(true)); + } + jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } + + private static void writeDirEntry(JarOutputStream jarOutputStream, String name) throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.closeEntry(); + } + + private static void writeEntry(JarOutputStream jarOutputStream, String name, int data) throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.write(new byte[] { (byte) data }); + jarOutputStream.closeEntry(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java new file mode 100644 index 00000000000..fbab8d36ed0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WarLauncher}. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +class WarLauncherTests extends AbstractExecutableArchiveLauncherTests { + + @Test + void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF")); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); + for (Archive archive : archives) { + archive.close(); + } + } + + @Test + void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { + File jarRoot = createJarArchive("archive.war", "WEB-INF"); + try (JarFileArchive archive = new JarFileArchive(jarRoot)) { + WarLauncher launcher = new WarLauncher(archive); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); + assertThat(getUrls(classPathArchives)).containsOnly( + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/")); + for (Archive classPathArchive : classPathArchives) { + classPathArchive.close(); + } + } + } + + @Test + void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList())); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + + @Test + void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception { + ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs)); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); + URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactly(expectedFileUrls); + } + + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + + protected final List getExpectedFilesWithExtraLibs(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/extra-1.jar")); + expected.add(new File(parent, "WEB-INF/lib/extra-2.jar")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java new file mode 100755 index 00000000000..77d2ce185c4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.archive; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExplodedArchive}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + */ +class ExplodedArchiveTests { + + @TempDir + File tempDir; + + private File rootDirectory; + + private ExplodedArchive archive; + + @BeforeEach + void setup() throws Exception { + createArchive(); + } + + @AfterEach + void tearDown() throws Exception { + if (this.archive != null) { + this.archive.close(); + } + } + + private void createArchive() throws Exception { + createArchive(null); + } + + private void createArchive(String directoryName) throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName) + : new File(this.tempDir, UUID.randomUUID().toString())); + JarFile jarFile = new JarFile(file); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); + destination.getParentFile().mkdirs(); + if (entry.isDirectory()) { + destination.mkdir(); + } + else { + FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination)); + } + } + this.archive = new ExplodedArchive(this.rootDirectory); + jarFile.close(); + } + + @Test + void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Map entries = getEntriesMap(this.archive); + assertThat(entries).hasSize(12); + } + + @Test + void getUrl() throws Exception { + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); + } + + @Test + void getUrlWithSpaceInPath() throws Exception { + createArchive("spaces in the name"); + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); + } + + @Test + void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + Archive nested = this.archive.getNestedArchive(entry); + assertThat(nested.getUrl()).hasToString(this.rootDirectory.toURI() + "nested.jar"); + nested.close(); + } + + @Test + void nestedDirArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("d/"); + Archive nested = this.archive.getNestedArchive(entry); + Map nestedEntries = getEntriesMap(nested); + assertThat(nestedEntries).hasSize(1); + assertThat(nested.getUrl()).hasToString("file:" + this.rootDirectory.toURI().getPath() + "d/"); + } + + @Test + void getNonRecursiveEntriesForRoot() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("/"), false)) { + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSizeGreaterThan(1); + } + } + + @Test + void getNonRecursiveManifest() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { + assertThat(explodedArchive.getManifest()).isNotNull(); + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSize(4); + } + } + + @Test + void getNonRecursiveManifestEvenIfNonRecursive() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { + assertThat(explodedArchive.getManifest()).isNotNull(); + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSize(3); + } + } + + @Test + void getResourceAsStream() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { + assertThat(explodedArchive.getManifest()).isNotNull(); + URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); + assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); + loader.close(); + } + } + + @Test + void getResourceAsStreamNonRecursive() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { + assertThat(explodedArchive.getManifest()).isNotNull(); + URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); + assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); + loader.close(); + } + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap<>(); + for (Archive.Entry entry : archive) { + entries.put(entry.getName(), entry); + } + return entries; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java new file mode 100755 index 00000000000..4b2ce93af63 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.archive; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.boot.loader.jar.JarFile; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarFileArchive}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Camille Vienot + */ +class JarFileArchiveTests { + + @TempDir + File tempDir; + + private File rootJarFile; + + private JarFileArchive archive; + + private String rootJarFileUrl; + + @BeforeEach + void setup() throws Exception { + setup(false); + } + + @AfterEach + void tearDown() throws Exception { + this.archive.close(); + } + + private void setup(boolean unpackNested) throws Exception { + this.rootJarFile = new File(this.tempDir, "root.jar"); + this.rootJarFileUrl = this.rootJarFile.toURI().toString(); + TestJarCreator.createTestJar(this.rootJarFile, unpackNested); + if (this.archive != null) { + this.archive.close(); + } + this.archive = new JarFileArchive(this.rootJarFile); + } + + @Test + void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Map entries = getEntriesMap(this.archive); + assertThat(entries).hasSize(12); + } + + @Test + void getUrl() throws Exception { + URL url = this.archive.getUrl(); + assertThat(url).hasToString(this.rootJarFileUrl); + } + + @Test + void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive nested = this.archive.getNestedArchive(entry)) { + assertThat(nested.getUrl()).hasToString("jar:" + this.rootJarFileUrl + "!/nested.jar!/"); + } + } + + @Test + void getNestedUnpackedArchive() throws Exception { + setup(true); + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive nested = this.archive.getNestedArchive(entry)) { + assertThat(nested.getUrl().toString()).startsWith("file:"); + assertThat(nested.getUrl().toString()).endsWith("/nested.jar"); + } + } + + @Test + void unpackedLocationsAreUniquePerArchive() throws Exception { + setup(true); + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + URL firstNestedUrl; + try (Archive firstNested = this.archive.getNestedArchive(entry)) { + firstNestedUrl = firstNested.getUrl(); + } + this.archive.close(); + setup(true); + entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive secondNested = this.archive.getNestedArchive(entry)) { + URL secondNestedUrl = secondNested.getUrl(); + assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl); + } + } + + @Test + void unpackedLocationsFromSameArchiveShareSameParent() throws Exception { + setup(true); + try (Archive nestedArchive = this.archive.getNestedArchive(getEntriesMap(this.archive).get("nested.jar")); + Archive anotherNestedArchive = this.archive + .getNestedArchive(getEntriesMap(this.archive).get("another-nested.jar"))) { + File nested = new File(nestedArchive.getUrl().toURI()); + File anotherNested = new File(anotherNestedArchive.getUrl().toURI()); + assertThat(nested).hasParent(anotherNested.getParent()); + } + } + + @Test + void filesInZip64ArchivesAreAllListed() throws IOException { + File file = new File(this.tempDir, "test.jar"); + FileCopyUtils.copy(writeZip64Jar(), file); + try (JarFileArchive zip64Archive = new JarFileArchive(file)) { + @SuppressWarnings("deprecation") + Iterator entries = zip64Archive.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(entries.hasNext()).as(i + "nth file is present").isTrue(); + entries.next(); + } + } + } + + @Test + void nestedZip64ArchivesAreHandledGracefully() throws Exception { + File file = new File(this.tempDir, "test.jar"); + try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) { + JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar"); + output.putNextEntry(zip64JarEntry); + byte[] zip64JarData = writeZip64Jar(); + zip64JarEntry.setSize(zip64JarData.length); + zip64JarEntry.setCompressedSize(zip64JarData.length); + zip64JarEntry.setMethod(ZipEntry.STORED); + CRC32 crc32 = new CRC32(); + crc32.update(zip64JarData); + zip64JarEntry.setCrc(crc32.getValue()); + output.write(zip64JarData); + output.closeEntry(); + } + try (JarFile jarFile = new JarFile(file)) { + ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar"); + try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) { + Iterator iterator = nestedJarFile.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue(); + iterator.next(); + } + } + } + } + + private byte[] writeZip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (JarOutputStream jarOutput = new JarOutputStream(bytes)) { + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.closeEntry(); + } + } + return bytes.toByteArray(); + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap<>(); + for (Archive.Entry entry : archive) { + entries.put(entry.getName(), entry); + } + return entries; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java new file mode 100644 index 00000000000..6713814def7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java @@ -0,0 +1,300 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.data; + +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * Tests for {@link RandomAccessDataFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class RandomAccessDataFileTests { + + private static final byte[] BYTES; + + static { + BYTES = new byte[256]; + for (int i = 0; i < BYTES.length; i++) { + BYTES[i] = (byte) i; + } + } + + private File tempFile; + + private RandomAccessDataFile file; + + private InputStream inputStream; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.tempFile = new File(tempDir, "tempFile"); + FileOutputStream outputStream = new FileOutputStream(this.tempFile); + outputStream.write(BYTES); + outputStream.close(); + this.file = new RandomAccessDataFile(this.tempFile); + this.inputStream = this.file.getInputStream(); + } + + @AfterEach + void cleanup() throws Exception { + this.inputStream.close(); + this.file.close(); + } + + @Test + void fileNotNull() { + assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(null)) + .withMessageContaining("File must not be null"); + } + + @Test + void fileExists() { + File file = new File("/does/not/exist"); + assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(file)) + .withMessageContaining(String.format("File %s must exist", file.getAbsolutePath())); + } + + @Test + void readWithOffsetAndLengthShouldRead() throws Exception { + byte[] read = this.file.read(2, 3); + assertThat(read).isEqualTo(new byte[] { 2, 3, 4 }); + } + + @Test + void readWhenOffsetIsBeyondEOFShouldThrowException() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0)); + } + + @Test + void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() { + RandomAccessData subsection = this.file.getSubsection(0, 10); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0)); + } + + @Test + void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() { + assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1)); + } + + @Test + void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() { + RandomAccessData subsection = this.file.getSubsection(0, 10); + assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1)); + } + + @Test + void inputStreamRead() throws Exception { + for (int i = 0; i <= 255; i++) { + assertThat(this.inputStream.read()).isEqualTo(i); + } + } + + @Test + void inputStreamReadNullBytes() { + assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null)) + .withMessage("Bytes must not be null"); + } + + @Test + void inputStreamReadNullBytesWithOffset() { + assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1)) + .withMessage("Bytes must not be null"); + } + + @Test + void inputStreamReadBytes() throws Exception { + byte[] b = new byte[256]; + int amountRead = this.inputStream.read(b); + assertThat(b).isEqualTo(BYTES); + assertThat(amountRead).isEqualTo(256); + } + + @Test + void inputStreamReadOffsetBytes() throws Exception { + byte[] b = new byte[7]; + this.inputStream.skip(1); + int amountRead = this.inputStream.read(b, 2, 3); + assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 }); + assertThat(amountRead).isEqualTo(3); + } + + @Test + void inputStreamReadMoreBytesThanAvailable() throws Exception { + byte[] b = new byte[257]; + int amountRead = this.inputStream.read(b); + assertThat(b).startsWith(BYTES); + assertThat(amountRead).isEqualTo(256); + } + + @Test + void inputStreamReadPastEnd() throws Exception { + this.inputStream.skip(255); + assertThat(this.inputStream.read()).isEqualTo(0xFF); + assertThat(this.inputStream.read()).isEqualTo(-1); + assertThat(this.inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamReadZeroLength() throws Exception { + byte[] b = new byte[] { 0x0F }; + int amountRead = this.inputStream.read(b, 0, 0); + assertThat(b).isEqualTo(new byte[] { 0x0F }); + assertThat(amountRead).isZero(); + assertThat(this.inputStream.read()).isZero(); + } + + @Test + void inputStreamSkip() throws Exception { + long amountSkipped = this.inputStream.skip(4); + assertThat(this.inputStream.read()).isEqualTo(4); + assertThat(amountSkipped).isEqualTo(4L); + } + + @Test + void inputStreamSkipMoreThanAvailable() throws Exception { + long amountSkipped = this.inputStream.skip(257); + assertThat(this.inputStream.read()).isEqualTo(-1); + assertThat(amountSkipped).isEqualTo(256L); + } + + @Test + void inputStreamSkipPastEnd() throws Exception { + this.inputStream.skip(256); + long amountSkipped = this.inputStream.skip(1); + assertThat(amountSkipped).isZero(); + } + + @Test + void inputStreamAvailable() throws Exception { + assertThat(this.inputStream.available()).isEqualTo(256); + this.inputStream.skip(56); + assertThat(this.inputStream.available()).isEqualTo(200); + this.inputStream.skip(200); + assertThat(this.inputStream.available()).isZero(); + } + + @Test + void subsectionNegativeOffset() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(-1, 1)); + } + + @Test + void subsectionNegativeLength() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, -1)); + } + + @Test + void subsectionZeroLength() throws Exception { + RandomAccessData subsection = this.file.getSubsection(0, 0); + assertThat(subsection.getInputStream().read()).isEqualTo(-1); + } + + @Test + void subsectionTooBig() { + this.file.getSubsection(0, 256); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, 257)); + } + + @Test + void subsectionTooBigWithOffset() { + this.file.getSubsection(1, 255); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(1, 256)); + } + + @Test + void subsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 1); + assertThat(subsection.getInputStream().read()).isOne(); + } + + @Test + void inputStreamReadPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + assertThat(inputStream.read()).isOne(); + assertThat(inputStream.read()).isEqualTo(2); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamReadBytesPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + byte[] b = new byte[3]; + int amountRead = inputStream.read(b); + assertThat(b).isEqualTo(new byte[] { 1, 2, 0 }); + assertThat(amountRead).isEqualTo(2); + } + + @Test + void inputStreamSkipPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + assertThat(inputStream.skip(3)).isEqualTo(2L); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamSkipNegative() throws Exception { + assertThat(this.inputStream.skip(-1)).isZero(); + } + + @Test + void getFile() { + assertThat(this.file.getFile()).isEqualTo(this.tempFile); + } + + @Test + void concurrentReads() throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(20); + List> results = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + results.add(executorService.submit(() -> { + InputStream subsectionInputStream = RandomAccessDataFileTests.this.file.getSubsection(0, 256) + .getInputStream(); + byte[] b = new byte[256]; + subsectionInputStream.read(b); + return Arrays.equals(b, BYTES); + })); + } + for (Future future : results) { + assertThat(future.get()).isTrue(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java new file mode 100644 index 00000000000..dd250501638 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AsciiBytes}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class AsciiBytesTests { + + private static final char NO_SUFFIX = 0; + + @Test + void createFromBytes() { + AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 }); + assertThat(bytes).hasToString("AB"); + } + + @Test + void createFromBytesWithOffset() { + AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); + assertThat(bytes).hasToString("BC"); + } + + @Test + void createFromString() { + AsciiBytes bytes = new AsciiBytes("AB"); + assertThat(bytes).hasToString("AB"); + } + + @Test + void length() { + AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); + assertThat(b1.length()).isEqualTo(2); + assertThat(b2.length()).isEqualTo(2); + } + + @Test + void startWith() { + AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); + AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abc.startsWith(abc)).isTrue(); + assertThat(abc.startsWith(ab)).isTrue(); + assertThat(abc.startsWith(bc)).isFalse(); + assertThat(abc.startsWith(abcd)).isFalse(); + } + + @Test + void endsWith() { + AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); + AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 }); + assertThat(abc.endsWith(abc)).isTrue(); + assertThat(abc.endsWith(bc)).isTrue(); + assertThat(abc.endsWith(ab)).isFalse(); + assertThat(abc.endsWith(aabc)).isFalse(); + } + + @Test + void substringFromBeingIndex() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abcd.substring(0)).hasToString("ABCD"); + assertThat(abcd.substring(1)).hasToString("BCD"); + assertThat(abcd.substring(2)).hasToString("CD"); + assertThat(abcd.substring(3)).hasToString("D"); + assertThat(abcd.substring(4).toString()).isEmpty(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(5)); + } + + @Test + void substring() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abcd.substring(0, 4)).hasToString("ABCD"); + assertThat(abcd.substring(1, 3)).hasToString("BC"); + assertThat(abcd.substring(3, 4)).hasToString("D"); + assertThat(abcd.substring(3, 3).toString()).isEmpty(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(3, 5)); + } + + @Test + void hashCodeAndEquals() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 }); + AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 }).substring(1, 3); + AsciiBytes bc_string = new AsciiBytes("BC"); + assertThat(bc).hasSameHashCodeAs(bc); + assertThat(bc).hasSameHashCodeAs(bc_substring); + assertThat(bc).hasSameHashCodeAs(bc_string); + assertThat(bc).isEqualTo(bc); + assertThat(bc).isEqualTo(bc_substring); + assertThat(bc).isEqualTo(bc_string); + assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode()); + assertThat(bc).isNotEqualTo(abcd); + } + + @Test + void hashCodeSameAsString() { + hashCodeSameAsString("abcABC123xyz!"); + } + + @Test + void hashCodeSameAsStringWithSpecial() { + hashCodeSameAsString("special/\u00EB.dat"); + } + + @Test + void hashCodeSameAsStringWithCyrillicCharacters() { + hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430"); + } + + @Test + void hashCodeSameAsStringWithEmoji() { + hashCodeSameAsString("\ud83d\udca9"); + } + + private void hashCodeSameAsString(String input) { + assertThat(new AsciiBytes(input)).hasSameHashCodeAs(input); + } + + @Test + void matchesSameAsString() { + matchesSameAsString("abcABC123xyz!"); + } + + @Test + void matchesSameAsStringWithSpecial() { + matchesSameAsString("special/\u00EB.dat"); + } + + @Test + void matchesSameAsStringWithCyrillicCharacters() { + matchesSameAsString("\u0432\u0435\u0441\u043D\u0430"); + } + + @Test + void matchesDifferentLengths() { + assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue(); + assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse(); + assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue(); + } + + @Test + void matchesSuffix() { + assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue(); + } + + @Test + void matchesSameAsStringWithEmoji() { + matchesSameAsString("\ud83d\udca9"); + } + + @Test + void hashCodeFromInstanceMatchesHashCodeFromString() { + String name = "fonts/宋体/simsun.ttf"; + assertThat(new AsciiBytes(name).hashCode()).isEqualTo(AsciiBytes.hashCode(name)); + } + + @Test + void instanceCreatedFromCharSequenceMatchesSameCharSequence() { + String name = "fonts/宋体/simsun.ttf"; + assertThat(new AsciiBytes(name).matches(name, NO_SUFFIX)).isTrue(); + } + + private void matchesSameAsString(String input) { + assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java new file mode 100644 index 00000000000..4d15c21fe30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessDataFile; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CentralDirectoryParser}. + * + * @author Phillip Webb + */ +class CentralDirectoryParserTests { + + private File jarFile; + + private RandomAccessDataFile jarData; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.jarFile = new File(tempDir, "test.jar"); + TestJarCreator.createTestJar(this.jarFile); + this.jarData = new RandomAccessDataFile(this.jarFile); + } + + @AfterEach + void tearDown() throws IOException { + this.jarData.close(); + } + + @Test + void visitsInOrder() throws Exception { + MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor(); + CentralDirectoryParser parser = new CentralDirectoryParser(); + parser.addVisitor(visitor); + parser.parse(this.jarData, false); + List invocations = visitor.getInvocations(); + assertThat(invocations).startsWith("visitStart").endsWith("visitEnd").contains("visitFileHeader"); + } + + @Test + void visitRecords() throws Exception { + Collector collector = new Collector(); + CentralDirectoryParser parser = new CentralDirectoryParser(); + parser.addVisitor(collector); + parser.parse(this.jarData, false); + Iterator headers = collector.getHeaders().iterator(); + assertThat(headers.next().getName()).hasToString("META-INF/"); + assertThat(headers.next().getName()).hasToString("META-INF/MANIFEST.MF"); + assertThat(headers.next().getName()).hasToString("1.dat"); + assertThat(headers.next().getName()).hasToString("2.dat"); + assertThat(headers.next().getName()).hasToString("d/"); + assertThat(headers.next().getName()).hasToString("d/9.dat"); + assertThat(headers.next().getName()).hasToString("special/"); + assertThat(headers.next().getName()).hasToString("special/\u00EB.dat"); + assertThat(headers.next().getName()).hasToString("nested.jar"); + assertThat(headers.next().getName()).hasToString("another-nested.jar"); + assertThat(headers.next().getName()).hasToString("space nested.jar"); + assertThat(headers.next().getName()).hasToString("multi-release.jar"); + assertThat(headers.hasNext()).isFalse(); + } + + static class Collector implements CentralDirectoryVisitor { + + private final List headers = new ArrayList<>(); + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + this.headers.add(fileHeader.clone()); + } + + @Override + public void visitEnd() { + } + + List getHeaders() { + return this.headers; + } + + } + + static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor { + + private final List invocations = new ArrayList<>(); + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + this.invocations.add("visitStart"); + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + this.invocations.add("visitFileHeader"); + } + + @Override + public void visitEnd() { + this.invocations.add("visitEnd"); + } + + List getInvocations() { + return this.invocations; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java new file mode 100644 index 00000000000..1a64de64312 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Handler}. + * + * @author Andy Wilkinson + */ +@ExtendWith(JarUrlProtocolHandler.class) +class HandlerTests { + + private final Handler handler = new Handler(); + + @Test + void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { + String spec = "/entry.txt"; + URL context = createUrl("file:example.jar!/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { + String spec = "/entry.txt"; + URL context = createUrl("file:example.jar!/dir/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/dir/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); + } + + @Test + void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/dir/file"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); + } + + @Test + void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException { + JarFile.registerUrlProtocolHandler(); + String spec = "jar:file:/other.jar!/nested!/entry.txt"; + URL context = createUrl("file:example.jar!/dir/file"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt"); + } + + @Test + void sameFileReturnsFalseForUrlsWithDifferentProtocols() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"), new URL("file:/foo.jar"))).isFalse(); + } + + @Test + void sameFileReturnsFalseForDifferentFileInSameJar() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"), + new URL("jar:file:/foo.jar!/content.txt"))) + .isFalse(); + } + + @Test + void sameFileReturnsFalseForSameFileInDifferentJars() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), + new URL("jar:file:/second.jar!/content.txt"))) + .isFalse(); + } + + @Test + void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), + new URL("jar:file:/the/path/to/the/first.jar!/content.txt"))) + .isTrue(); + } + + @Test + void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() + throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"), + new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))) + .isTrue(); + } + + @Test + void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() throws MalformedURLException { + assertThat(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"))) + .isEqualTo(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))); + } + + @Test + void urlWithSpecReferencingParentDirectory() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../directoryB/c/d/e.xsd"); + } + + @Test + void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../../../../../../directoryB/b.xsd"); + } + + @Test + void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "./directoryB/c/d/e.xsd"); + } + + @Test + void urlWithRef() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt#alpha"); + } + + @Test + void urlWithQuery() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt?alpha"); + } + + @Test + void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "test.jar"); + TestJarCreator.createTestJar(testJar); + URLConnection connection = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler) + .openConnection(); + assertThat(connection).isInstanceOf(JarURLConnection.class); + ((JarURLConnection) connection).getJarFile().close(); + URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/", + this.handler) + .openConnection(); + assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class); + assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection"); + } + + @Test + void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "t+e+s+t.jar"); + TestJarCreator.createTestJar(testJar); + URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); + } + } + + @Test + void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "t e s t.jar"); + TestJarCreator.createTestJar(testJar); + URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); + } + } + + private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec) throws MalformedURLException { + URL standardUrl = new URL(new URL("jar:" + context), spec); + URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler), spec); + assertThat(customHandlerUrl).hasToString(standardUrl.toString()); + assertThat(customHandlerUrl.getFile()).isEqualTo(standardUrl.getFile()); + assertThat(customHandlerUrl.getPath()).isEqualTo(standardUrl.getPath()); + assertThat(customHandlerUrl.getQuery()).isEqualTo(standardUrl.getQuery()); + assertThat(customHandlerUrl.getRef()).isEqualTo(standardUrl.getRef()); + } + + private URL createUrl(String file) throws MalformedURLException { + return new URL("jar", null, -1, file, this.handler); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java new file mode 100644 index 00000000000..b37a99183a7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -0,0 +1,736 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.data.RandomAccessDataFile; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link JarFile}. + * + * @author Phillip Webb + * @author Martin Lau + * @author Andy Wilkinson + * @author Madhura Bhave + */ +@ExtendWith(JarUrlProtocolHandler.class) +class JarFileTests { + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; + + @TempDir + File tempDir; + + private File rootJarFile; + + private JarFile jarFile; + + @BeforeEach + void setup() throws Exception { + this.rootJarFile = new File(this.tempDir, "root.jar"); + TestJarCreator.createTestJar(this.rootJarFile); + this.jarFile = new JarFile(this.rootJarFile); + } + + @AfterEach + void tearDown() throws Exception { + this.jarFile.close(); + } + + @Test + void jdkJarFile() throws Exception { + // Sanity checks to see how the default jar file operates + java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile); + assertThat(jarFile.getComment()).isEqualTo("outer"); + Enumeration entries = jarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("d/"); + assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("special/"); + assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); + assertThat(entries.hasMoreElements()).isFalse(); + URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); + assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull(); + urlClassLoader.close(); + jarFile.close(); + } + + @Test + void createFromFile() throws Exception { + JarFile jarFile = new JarFile(this.rootJarFile); + assertThat(jarFile.getName()).isNotNull(); + jarFile.close(); + } + + @Test + void getManifest() throws Exception { + assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getManifestEntry() throws Exception { + ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF"); + Manifest manifest = new Manifest(this.jarFile.getInputStream(entry)); + assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Enumeration entries = this.jarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("d/"); + assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("special/"); + assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); + assertThat(entries.hasMoreElements()).isFalse(); + } + + @Test + void getSpecialResourceViaClassLoader() throws Exception { + URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { this.jarFile.getUrl() }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); + urlClassLoader.close(); + } + + @Test + void getJarEntry() { + java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat"); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + + @Test + void getJarEntryWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat")); + } + + @Test + void getInputStream() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat")); + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isOne(); + assertThat(inputStream.available()).isZero(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void getInputStreamWhenClosed() throws Exception { + ZipEntry entry = this.jarFile.getEntry("1.dat"); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry)); + } + + @Test + void getComment() { + assertThat(this.jarFile.getComment()).isEqualTo("outer"); + } + + @Test + void getCommentWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment()); + } + + @Test + void getName() { + assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath()); + } + + @Test + void size() throws Exception { + try (ZipFile zip = new ZipFile(this.rootJarFile)) { + assertThat(this.jarFile).hasSize(zip.size()); + } + } + + @Test + void sizeWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size()); + } + + @Test + void getEntryTime() throws Exception { + java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile); + assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime()) + .isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime()); + jdkJarFile.close(); + } + + @Test + void close() throws Exception { + RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile)); + JarFile jarFile = new JarFile(randomAccessDataFile); + jarFile.close(); + then(randomAccessDataFile).should().close(); + } + + @Test + void getUrl() throws Exception { + URL url = this.jarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/"); + JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getJarEntry()).isNull(); + assertThat(jarURLConnection.getContentLength()).isGreaterThan(1); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar"); + assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); + } + + @Test + void createEntryUrl() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "1.dat"); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/1.dat"); + JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat")); + assertThat(jarURLConnection.getContentLength()).isOne(); + assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class); + assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown"); + assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class); + FilePermission permission = (FilePermission) jarURLConnection.getPermission(); + assertThat(permission.getActions()).isEqualTo("read"); + assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); + } + + @Test + void getMissingEntryUrl() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "missing.dat"); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/missing.dat"); + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(((JarURLConnection) url.openConnection())::getJarEntry); + } + + @Test + void getUrlStream() throws Exception { + URL url = this.jarFile.getUrl(); + url.openConnection(); + assertThatIOException().isThrownBy(url::openStream); + } + + @Test + void getEntryUrlStream() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "1.dat"); + url.openConnection(); + try (InputStream stream = url.openStream()) { + assertThat(stream.read()).isOne(); + assertThat(stream.read()).isEqualTo(-1); + } + } + + @Test + void getNestedJarFile() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThat(nestedJarFile.getComment()).isEqualTo("nested"); + Enumeration entries = nestedJarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("3.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("4.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat"); + assertThat(entries.hasMoreElements()).isFalse(); + + InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("3.dat")); + assertThat(inputStream.read()).isEqualTo(3); + assertThat(inputStream.read()).isEqualTo(-1); + + URL url = nestedJarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/"); + JarURLConnection conn = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile); + assertThat(conn.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); + assertThat(conn.getInputStream()).isNotNull(); + JarInputStream jarInputStream = new JarInputStream(conn.getInputStream()); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat"); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat"); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat"); + jarInputStream.close(); + assertThat(conn.getPermission()).isInstanceOf(FilePermission.class); + FilePermission permission = (FilePermission) conn.getPermission(); + assertThat(permission.getActions()).isEqualTo("read"); + assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); + } + } + + @Test + void getNestedJarDirectory() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("d/"))) { + Enumeration entries = nestedJarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("9.dat"); + assertThat(entries.hasMoreElements()).isFalse(); + + try (InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("9.dat"))) { + assertThat(inputStream.read()).isEqualTo(9); + assertThat(inputStream.read()).isEqualTo(-1); + } + + URL url = nestedJarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/d!/"); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile); + } + } + + @Test + void getNestedJarEntryUrl() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL url = nestedJarFile.getJarEntry("3.dat").getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"); + try (InputStream inputStream = url.openStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(3); + } + } + } + + @Test + void createUrlFromString() throws Exception { + String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"; + URL url = new URL(spec); + assertThat(url).hasToString(spec); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (InputStream inputStream = connection.getInputStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(3); + assertThat(connection.getURL()).hasToString(spec); + assertThat(connection.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); + assertThat(connection.getEntryName()).isEqualTo("3.dat"); + connection.getJarFile().close(); + } + } + + @Test + void createNonNestedUrlFromString() throws Exception { + nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat"); + } + + @Test + void createNonNestedUrlFromPathString() throws Exception { + nonNestedJarFileFromString("jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat"); + } + + private void nonNestedJarFileFromString(String spec) throws Exception { + JarFile.registerUrlProtocolHandler(); + URL url = new URL(spec); + assertThat(url).hasToString(spec); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (InputStream inputStream = connection.getInputStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(2); + assertThat(connection.getURL()).hasToString(spec); + assertThat(connection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); + assertThat(connection.getEntryName()).isEqualTo("2.dat"); + } + connection.getJarFile().close(); + } + + @Test + void getDirectoryInputStream() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d/")); + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void getDirectoryInputStreamWithoutSlash() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d")); + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void sensibleToString() throws Exception { + assertThat(this.jarFile).hasToString(this.rootJarFile.getPath()); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThat(nested).hasToString(this.rootJarFile.getPath() + "!/nested.jar"); + } + } + + @Test + void verifySignedJar() throws Exception { + File signedJarFile = getSignedJarFile(); + assertThat(signedJarFile).exists(); + try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) { + try (JarFile actual = new JarFile(signedJarFile)) { + StopWatch stopWatch = new StopWatch(); + Enumeration actualEntries = actual.entries(); + while (actualEntries.hasMoreElements()) { + JarEntry actualEntry = actualEntries.nextElement(); + java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); + StreamUtils.drain(expected.getInputStream(expectedEntry)); + if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) { + assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCertificates()); + assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCodeSigners()); + } + } + assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); + } + } + } + + private File getSignedJarFile() { + String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); + for (String entry : entries) { + if (entry.contains("bcprov")) { + return new File(entry); + } + } + return null; + } + + @Test + void jarFileWithScriptAtTheStart() throws Exception { + File file = new File(this.tempDir, "test.jar"); + InputStream sourceJarContent = new FileInputStream(this.rootJarFile); + FileOutputStream outputStream = new FileOutputStream(file); + StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream); + FileCopyUtils.copy(sourceJarContent, outputStream); + this.rootJarFile = file; + this.jarFile.close(); + this.jarFile = new JarFile(file); + // Call some other tests to verify + getEntries(); + getNestedJarFile(); + } + + @Test + void cannotLoadMissingJar() throws Exception { + // relates to gh-1070 + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL nestedUrl = nestedJarFile.getUrl(); + URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat"); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(url.openConnection()::getInputStream); + } + } + + @Test + void registerUrlProtocolHandlerWithNoExistingRegistration() { + String original = System.getProperty(PROTOCOL_HANDLER); + try { + System.clearProperty(PROTOCOL_HANDLER); + JarFile.registerUrlProtocolHandler(); + String protocolHandler = System.getProperty(PROTOCOL_HANDLER); + assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE); + } + finally { + if (original == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, original); + } + } + } + + @Test + void registerUrlProtocolHandlerAddsToExistingRegistration() { + String original = System.getProperty(PROTOCOL_HANDLER); + try { + System.setProperty(PROTOCOL_HANDLER, "com.example"); + JarFile.registerUrlProtocolHandler(); + String protocolHandler = System.getProperty(PROTOCOL_HANDLER); + assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE); + } + finally { + if (original == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, original); + } + } + } + + @Test + void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception { + File jar = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(jar); + JarFile jf = new JarFile(jar); + jf.close(); + assertThat(jar.delete()).isTrue(); + } + + @Test + void createUrlFromStringWithContextWhenNotFound() throws Exception { + // gh-12483 + JarURLConnection.setUseFastExceptions(true); + try { + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL context = nested.getUrl(); + new URL(context, "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat").openConnection() + .getInputStream() + .close(); + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(new URL(context, "jar:" + this.rootJarFile.toURI() + "!/no.dat") + .openConnection()::getInputStream); + } + } + finally { + JarURLConnection.setUseFastExceptions(false); + } + } + + @Test + void multiReleaseEntry() throws Exception { + try (JarFile multiRelease = this.jarFile.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"))) { + ZipEntry entry = multiRelease.getEntry("multi-release.dat"); + assertThat(entry.getName()).isEqualTo("multi-release.dat"); + InputStream inputStream = multiRelease.getInputStream(entry); + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isEqualTo(Runtime.version().feature()); + } + } + + @Test + void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception { + File zip64Jar = new File(this.tempDir, "zip64.jar"); + FileCopyUtils.copy(zip64Jar(), zip64Jar); + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + List entries = Collections.list(zip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = zip64JarFile.getInputStream(entry); + assertThat(entryInput).hasContent("Entry " + (i + 1)); + } + } + } + + @Test + void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception { + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + File zip64Jar = new File(this.tempDir, "zip64.jar"); + File entry = new File(this.tempDir, "entry.dat"); + CRC32 crc32 = new CRC32(); + try (FileOutputStream entryOut = new FileOutputStream(entry)) { + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + for (int i = 0; i < 1024; i++) { + entryOut.write(data); + crc32.update(data); + } + } + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) { + for (int i = 0; i < 6; i++) { + JarEntry storedEntry = new JarEntry("huge-" + i); + storedEntry.setSize(entry.length()); + storedEntry.setCompressedSize(entry.length()); + storedEntry.setCrc(crc32.getValue()); + storedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(storedEntry); + try (FileInputStream entryIn = new FileInputStream(entry)) { + StreamUtils.copy(entryIn, jarOutput); + } + jarOutput.closeEntry(); + } + } + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + assertThat(Collections.list(zip64JarFile.entries())).hasSize(6); + } + } + + @Test + void nestedZip64JarCanBeRead() throws Exception { + File outer = new File(this.tempDir, "outer.jar"); + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) { + JarEntry nestedEntry = new JarEntry("nested-zip64.jar"); + byte[] contents = zip64Jar(); + nestedEntry.setSize(contents.length); + nestedEntry.setCompressedSize(contents.length); + CRC32 crc32 = new CRC32(); + crc32.update(contents); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(nestedEntry); + jarOutput.write(contents); + jarOutput.closeEntry(); + } + try (JarFile outerJarFile = new JarFile(outer)) { + try (JarFile nestedZip64JarFile = outerJarFile + .getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) { + List entries = Collections.list(nestedZip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = nestedZip64JarFile.getInputStream(entry); + assertThat(entryInput).hasContent("Entry " + (i + 1)); + } + } + } + } + + private byte[] zip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JarOutputStream jarOutput = new JarOutputStream(bytes); + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8)); + jarOutput.closeEntry(); + } + jarOutput.close(); + return bytes.toByteArray(); + } + + @Test + void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { + File file = createJarFileWithEpochTimeOfZero(); + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + JarEntry entry = entries.nextElement(); + assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + } + + private File createJarFileWithEpochTimeOfZero() throws Exception { + File jarFile = new File(this.tempDir, "temp.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(jarFile); + String comment = "outer"; + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment(comment); + JarEntry entry = new JarEntry("1.dat"); + entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); + jarOutputStream.putNextEntry(entry); + jarOutputStream.write(new byte[] { (byte) 1 }); + jarOutputStream.closeEntry(); + } + + byte[] data = Files.readAllBytes(jarFile.toPath()); + int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length; + int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1); + int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1); + writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0); + writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0); + + File jar = new File(this.tempDir, "zerotimed.jar"); + Files.write(jar.toPath(), data); + return jar; + } + + private static void writeTimeBlock(byte[] data, int pos, int value) { + data[pos] = (byte) (value & 0xff); + data[pos + 1] = (byte) ((value >> 8) & 0xff); + data[pos + 2] = (byte) ((value >> 16) & 0xff); + data[pos + 3] = (byte) ((value >> 24) & 0xff); + } + + @Test + void iterator() { + Iterator iterator = this.jarFile.iterator(); + List names = new ArrayList<>(); + while (iterator.hasNext()) { + names.add(iterator.next().getName()); + } + assertThat(names).hasSize(12).contains("1.dat"); + } + + @Test + void iteratorWhenClosed() throws IOException { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator()); + } + + @Test + void iteratorWhenClosedLater() throws IOException { + Iterator iterator = this.jarFile.iterator(); + iterator.next(); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext()); + } + + @Test + void stream() { + Stream stream = this.jarFile.stream().map(JarEntry::getName); + assertThat(stream).hasSize(12).contains("1.dat"); + + } + + private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) { + assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java new file mode 100644 index 00000000000..8ae25b72e17 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.jar.JarFileWrapperTests.SpyJarFile.Call; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JarFileWrapper}. + * + * @author Phillip Webb + */ +class JarFileWrapperTests { + + private SpyJarFile parent; + + private JarFileWrapper wrapper; + + @BeforeEach + void setup(@TempDir File temp) throws Exception { + this.parent = new SpyJarFile(createTempJar(temp)); + this.wrapper = new JarFileWrapper(this.parent); + } + + @AfterEach + void cleanup() throws Exception { + this.parent.close(); + } + + private File createTempJar(File temp) throws IOException { + File file = new File(temp, "temp.jar"); + new JarOutputStream(new FileOutputStream(file)).close(); + return file; + } + + @Test + void getUrlDelegatesToParent() throws MalformedURLException { + this.wrapper.getUrl(); + this.parent.verify(Call.GET_URL); + } + + @Test + void getTypeDelegatesToParent() { + this.wrapper.getType(); + this.parent.verify(Call.GET_TYPE); + } + + @Test + void getPermissionDelegatesToParent() { + this.wrapper.getPermission(); + this.parent.verify(Call.GET_PERMISSION); + } + + @Test + void getManifestDelegatesToParent() throws IOException { + this.wrapper.getManifest(); + this.parent.verify(Call.GET_MANIFEST); + } + + @Test + void entriesDelegatesToParent() { + this.wrapper.entries(); + this.parent.verify(Call.ENTRIES); + } + + @Test + void getJarEntryDelegatesToParent() { + this.wrapper.getJarEntry("test"); + this.parent.verify(Call.GET_JAR_ENTRY); + } + + @Test + void getEntryDelegatesToParent() { + this.wrapper.getEntry("test"); + this.parent.verify(Call.GET_ENTRY); + } + + @Test + void getInputStreamDelegatesToParent() throws IOException { + this.wrapper.getInputStream(); + this.parent.verify(Call.GET_INPUT_STREAM); + } + + @Test + void getEntryInputStreamDelegatesToParent() throws IOException { + ZipEntry entry = new ZipEntry("test"); + this.wrapper.getInputStream(entry); + this.parent.verify(Call.GET_ENTRY_INPUT_STREAM); + } + + @Test + void getCommentDelegatesToParent() { + this.wrapper.getComment(); + this.parent.verify(Call.GET_COMMENT); + } + + @Test + void sizeDelegatesToParent() { + this.wrapper.size(); + this.parent.verify(Call.SIZE); + } + + @Test + void toStringDelegatesToParent() { + assertThat(this.wrapper.toString()).endsWith("temp.jar"); + } + + @Test // gh-22991 + void wrapperMustNotImplementClose() { + // If the wrapper overrides close then on Java 11 a FinalizableResource + // instance will be used to perform cleanup. This can result in a lot + // of additional memory being used since cleanup only occurs when the + // finalizer thread runs. See gh-22991 + assertThatExceptionOfType(NoSuchMethodException.class) + .isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close")); + } + + @Test + void streamDelegatesToParent() { + this.wrapper.stream(); + this.parent.verify(Call.STREAM); + } + + /** + * {@link JarFile} that we can spy (even on Java 11+) + */ + static class SpyJarFile extends JarFile { + + private final Set calls = EnumSet.noneOf(Call.class); + + SpyJarFile(File file) throws IOException { + super(file); + } + + @Override + Permission getPermission() { + mark(Call.GET_PERMISSION); + return super.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + mark(Call.GET_MANIFEST); + return super.getManifest(); + } + + @Override + public Enumeration entries() { + mark(Call.ENTRIES); + return super.entries(); + } + + @Override + public Stream stream() { + mark(Call.STREAM); + return super.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + mark(Call.GET_JAR_ENTRY); + return super.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + mark(Call.GET_ENTRY); + return super.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + mark(Call.GET_INPUT_STREAM); + return super.getInputStream(); + } + + @Override + InputStream getInputStream(String name) throws IOException { + mark(Call.GET_ENTRY_INPUT_STREAM); + return super.getInputStream(name); + } + + @Override + public String getComment() { + mark(Call.GET_COMMENT); + return super.getComment(); + } + + @Override + public int size() { + mark(Call.SIZE); + return super.size(); + } + + @Override + public URL getUrl() throws MalformedURLException { + mark(Call.GET_URL); + return super.getUrl(); + } + + @Override + JarFileType getType() { + mark(Call.GET_TYPE); + return super.getType(); + } + + private void mark(Call call) { + this.calls.add(call); + } + + void verify(Call call) { + assertThat(call).matches(this.calls::contains); + } + + enum Call { + + GET_URL, + + GET_TYPE, + + GET_PERMISSION, + + GET_MANIFEST, + + ENTRIES, + + GET_JAR_ENTRY, + + GET_ENTRY, + + GET_INPUT_STREAM, + + GET_ENTRY_INPUT_STREAM, + + GET_COMMENT, + + SIZE, + + STREAM + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java new file mode 100644 index 00000000000..d962a72fc57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.jar.JarEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JarURLConnection}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @author Rostyslav Dudka + */ +class JarURLConnectionTests { + + private File rootJarFile; + + private JarFile jarFile; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.rootJarFile = new File(tempDir, "root.jar"); + TestJarCreator.createTestJar(this.rootJarFile); + this.jarFile = new JarFile(this.rootJarFile); + } + + @AfterEach + void tearDown() throws Exception { + this.jarFile.close(); + } + + @Test + void connectionToRootUsingAbsoluteUrl() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); + } + + @Test + void connectionToRootUsingRelativeUrl() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/"); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); + } + + @Test + void connectionToEntryUsingAbsoluteUrl() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingRelativeUrl() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Exception { + URL url = new URL(new URL("jar", null, -1, this.rootJarFile.toURI().toURL() + "!/nested.jar!/", new Handler()), + "/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryWithSpaceNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingWrongAbsoluteUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/w.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(JarURLConnection.get(url, nested)::getInputStream); + } + } + + @Test + void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + JarURLConnection connection = JarURLConnection.get(url, nested); + assertThat(connection.getContentLength()).isOne(); + } + } + + @Test + void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + JarURLConnection connection = JarURLConnection.get(url, nested); + assertThat(connection.getContentLengthLong()).isOne(); + } + } + + @Test + void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime()); + } + + @Test + void entriesCanBeStreamedFromJarFileOfConnection() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + List entryNames = connection.getJarFile().stream().map(JarEntry::getName).toList(); + assertThat(entryNames).hasSize(12); + } + + @Test + void jarEntryBasicName() { + assertThat(new JarEntryName(new StringSequence("a/b/C.class"))).hasToString("a/b/C.class"); + } + + @Test + void jarEntryNameWithSingleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class"))).hasToString("a/b/C.class"); + } + + @Test + void jarEntryNameWithDoubleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class"))).hasToString("\u00e1/b/C.class"); + } + + @Test + void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { + assertThat(new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class"))).hasToString("\u00e1/b/\u00c7.class"); + } + + @Test + void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + java.util.jar.JarFile connectionJarFile = connection.getJarFile(); + connectionJarFile.close(); + assertThat(this.jarFile.isClosed()).isFalse(); + } + + private String getRelativePath() { + return this.rootJarFile.getPath().replace('\\', '/'); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java new file mode 100644 index 00000000000..d9e5eb28142 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import java.io.File; +import java.lang.ref.SoftReference; +import java.util.Map; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.test.util.ReflectionTestUtils; + +/** + * JUnit 5 {@link Extension} for tests that interact with Spring Boot's {@link Handler} + * for {@code jar:} URLs. Ensures that the handler is registered prior to test execution + * and cleans up the handler's root file cache afterwards. + * + * @author Andy Wilkinson + */ +class JarUrlProtocolHandler implements BeforeEachCallback, AfterEachCallback { + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + JarFile.registerUrlProtocolHandler(); + } + + @Override + @SuppressWarnings("unchecked") + public void afterEach(ExtensionContext context) throws Exception { + Map rootFileCache = ((SoftReference>) ReflectionTestUtils + .getField(Handler.class, "rootFileCache")).get(); + if (rootFileCache != null) { + for (JarFile rootJarFile : rootFileCache.values()) { + rootJarFile.close(); + } + rootFileCache.clear(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java new file mode 100644 index 00000000000..ee7170f08c2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * Tests for {@link StringSequence}. + * + * @author Phillip Webb + */ +class StringSequenceTests { + + @Test + void createWhenSourceIsNullShouldThrowException() { + assertThatNullPointerException().isThrownBy(() -> new StringSequence(null)) + .withMessage("Source must not be null"); + } + + @Test + void createWithIndexWhenSourceIsNullShouldThrowException() { + assertThatNullPointerException().isThrownBy(() -> new StringSequence(null, 0, 0)) + .withMessage("Source must not be null"); + } + + @Test + void createWhenStartIsLessThanZeroShouldThrowException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class) + .isThrownBy(() -> new StringSequence("x", -1, 0)); + } + + @Test + void createWhenEndIsGreaterThanLengthShouldThrowException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class) + .isThrownBy(() -> new StringSequence("x", 0, 2)); + } + + @Test + void createFromString() { + assertThat(new StringSequence("test")).hasToString("test"); + } + + @Test + void subSequenceWithJustStartShouldReturnSubSequence() { + assertThat(new StringSequence("smiles").subSequence(1)).hasToString("miles"); + } + + @Test + void subSequenceShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasToString("urge"); + assertThat(new StringSequence("smiles").subSequence(1, 5)).hasToString("mile"); + } + + @Test + void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)).hasToString("rg"); + } + + @Test + void subSequenceWhenEndPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence).hasToString("bcd"); + assertThat(sequence.subSequence(2, 3)).hasToString("d"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(3, 4)); + } + + @Test + void subSequenceWhenStartPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence).hasToString("bcd"); + assertThat(sequence.subSequence(2, 3)).hasToString("d"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(4, 3)); + } + + @Test + void isEmptyWhenEmptyShouldReturnTrue() { + assertThat(new StringSequence("").isEmpty()).isTrue(); + } + + @Test + void isEmptyWhenNotEmptyShouldReturnFalse() { + assertThat(new StringSequence("x").isEmpty()).isFalse(); + } + + @Test + void lengthShouldReturnLength() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence).hasSize(9); + assertThat(sequence.subSequence(4, 8)).hasSize(4); + } + + @Test + void charAtShouldReturnChar() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence.charAt(0)).isEqualTo('h'); + assertThat(sequence.charAt(1)).isEqualTo('a'); + assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u'); + assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r'); + } + + @Test + void indexOfCharShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf('a')).isZero(); + assertThat(sequence.indexOf('b')).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); + } + + @Test + void indexOfStringShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf('a')).isZero(); + assertThat(sequence.indexOf('b')).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); + } + + @Test + void indexOfStringFromIndexShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf("a", 2)).isEqualTo(4); + assertThat(sequence.indexOf("b", 3)).isEqualTo(3); + assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3); + } + + @Test + void hashCodeShouldBeSameAsString() { + assertThat(new StringSequence("hamburger")).hasSameHashCodeAs("hamburger"); + assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasSameHashCodeAs("urge"); + } + + @Test + void equalsWhenSameContentShouldMatch() { + StringSequence a = new StringSequence("hamburger").subSequence(4, 8); + StringSequence b = new StringSequence("urge"); + StringSequence c = new StringSequence("urgh"); + assertThat(a).isEqualTo(b).isNotEqualTo(c); + } + + @Test + void notEqualsWhenSequencesOfDifferentLength() { + StringSequence a = new StringSequence("abcd"); + StringSequence b = new StringSequence("ef"); + assertThat(a).isNotEqualTo(b); + } + + @Test + void startsWithWhenExactMatch() { + assertThat(new StringSequence("abc").startsWith("abc")).isTrue(); + } + + @Test + void startsWithWhenLongerAndStartsWith() { + assertThat(new StringSequence("abcd").startsWith("abc")).isTrue(); + } + + @Test + void startsWithWhenLongerAndDoesNotStartWith() { + assertThat(new StringSequence("abcd").startsWith("abx")).isFalse(); + } + + @Test + void startsWithWhenShorterAndDoesNotStartWith() { + assertThat(new StringSequence("ab").startsWith("abc")).isFalse(); + assertThat(new StringSequence("ab").startsWith("c")).isFalse(); + } + + @Test + void startsWithOffsetWhenExactMatch() { + assertThat(new StringSequence("xabc").startsWith("abc", 1)).isTrue(); + } + + @Test + void startsWithOffsetWhenLongerAndStartsWith() { + assertThat(new StringSequence("xabcd").startsWith("abc", 1)).isTrue(); + } + + @Test + void startsWithOffsetWhenLongerAndDoesNotStartWith() { + assertThat(new StringSequence("xabcd").startsWith("abx", 1)).isFalse(); + } + + @Test + void startsWithOffsetWhenShorterAndDoesNotStartWith() { + assertThat(new StringSequence("xab").startsWith("abc", 1)).isFalse(); + assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse(); + } + + @Test + void startsWithOnSubstringTailWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1); + assertThat(subSequence.startsWith("abc")).isTrue(); + assertThat(subSequence.startsWith("abcd")).isFalse(); + } + + @Test + void startsWithOnSubstringMiddleWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3); + assertThat(subSequence.startsWith("ab")).isTrue(); + assertThat(subSequence.startsWith("abc")).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java new file mode 100644 index 00000000000..dec587e18bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.jarmode; + +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.Launcher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Launcher} with jar mode support. + * + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +class LauncherJarModeTests { + + @BeforeEach + void setup() { + System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true"); + } + + @AfterEach + void cleanup() { + System.clearProperty("jarmode"); + System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT); + } + + @Test + void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("running in test jar mode [boot]"); + } + + @Test + void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "idontexist"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("Unsupported jarmode 'idontexist'"); + } + + private static class TestLauncher extends Launcher { + + @Override + protected String getMainClass() throws Exception { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + return Collections.emptyIterator(); + } + + @Override + protected void launch(String[] args) throws Exception { + super.launch(args); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java new file mode 100644 index 00000000000..802a762e79d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.util; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SystemPropertyUtils}. + * + * @author Dave Syer + */ +class SystemPropertyUtilsTests { + + @BeforeEach + void init() { + System.setProperty("foo", "bar"); + } + + @AfterEach + void close() { + System.clearProperty("foo"); + } + + @Test + void testVanillaPlaceholder() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar"); + } + + @Test + void testDefaultValue() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}")).isEqualTo("foo"); + } + + @Test + void testNestedPlaceholder() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}")).isEqualTo("foo"); + } + + @Test + void testEnvVar() { + assertThat(SystemPropertyUtils.getProperty("lang")).isEqualTo(System.getenv("LANG")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties new file mode 100644 index 00000000000..85a390f4d4e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties @@ -0,0 +1 @@ +loader.main: demo.Application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties new file mode 100644 index 00000000000..6b37480f8b9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties @@ -0,0 +1 @@ +loader.main: my.BootInfBarApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties new file mode 100644 index 00000000000..36bd211df41 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties @@ -0,0 +1,3 @@ +foo: Application +loader.main: my.${foo} +loader.path: etc diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties new file mode 100644 index 00000000000..85a390f4d4e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties @@ -0,0 +1 @@ +loader.main: demo.Application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories new file mode 100644 index 00000000000..c45c87d76f4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Jar Modes +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.loader.jarmode.TestJarMode \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties new file mode 100644 index 00000000000..8301c2649f3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties @@ -0,0 +1 @@ +loader.main: my.BarApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt new file mode 100644 index 00000000000..c53100f90fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2020 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 explodedsample; + +/** + * Example class used to test class loading. + * + * @author Phillip Webb + */ +public class ExampleClass { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties new file mode 100644 index 00000000000..7a134969b76 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties @@ -0,0 +1 @@ +loader.main: demo.HomeApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..fb02c027012d66154056f7e7df17ba836ee6d2b8 GIT binary patch literal 2213 zcmWIWW@Zs#;Nak3&?ukh!hi%g8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1ge|^G~JD{Zqb94n||$D z(D61fRetw|w~lFAvv#y+uRVA|;M5l0_^WGPN-h_^_Iq;SBBlGxlb4vEKK)8y_N^&Z zjG*Y)@p4|!I-oBk86hD7iA!a)kl;n~XL3$radB>-t^Z*I5u4=~mMBiSbm>#%0_LE^ z47OuRDlJ!5GPwwHPi9_Ll=&InLf~u=Nl}uDr0I+-r-AUK+P~A1r*5WP5;Z7Eh|s z4K?R3GwZp1-Der1Z=@AePHHhpS@GwlO|RRdTa)gY?DWW<&3!cH?vv?i`@Lq=|892w zBofSW=)!`3!VOm*N*YF?0binLOYR)_7$Oe~u{kG-Fg+n^F{ERQx%B(8!x2pY> z9h4f9{{7y8c`ut2`_4V`7xw2c_|JWJVQ72SOIE+FAG&7R@35Dw5RPMi+IfmeYrjkP z0dr7Xy-`}Rst_1Y8o>O6NVQjL=7Ewj9IygeDXF>nNXhmLni4UX632pqoXq6JlFa-( zJxII;#CkJ1ir6k*z4-OCWtsDr*|J=7=H4si=yI_+B;ks)|6{8YC*GXfv@XzowH#N~ z3-&LI7wwi#bz&~IKWF>=+}_V0e}3b8z>q6lsUsTby0#%zy41uX%py$kQ@d8cM?vRp zo2w5e%$>0?MdN#jg8s694sO?z7I1E?S;*Ap_I#bW*!M$Ml_YllwFo-6Vm8Oy%k2Jb z8;uUOB=>&WH`8sNO>pOqBAK?RN+r$r>P74_tbL{I(l=5x=6bL7{e8`)>cqv84Xt7; z+b)^Elh<_Iyj`C?`Q=fbt&(4?56Nu&Z_&BbpL>?(#n<;AYd?;=_{K0-q4oaXOk;lI z4+bYqOxKkEyDfeD#Kr4s>de!0T(@~dPPS2=f4iEsdQn4NOO73@v5V$GPumTXy&4}G z8(n?Fy+-oxGNFXP>oQ`OIua-5S>^`)WCF#K;p18cdtfX*K*>?3(V{>UTNHqb5Z`WF zzC#8)Zr|q|dUZu_uY)?9$~Qiq4n>u~j)ElZGH#Bq?^uo;xV>-w@oRT;cpf+}JNcpL z;FALjMH^*{xx<7-D8O6o$=K@uYc;A_o?&x7rk|KPx_xb?|s@w zU)Squ@mX)5Q=X+PU6#&Ag}jYEf6htXA;it~<<6(iJHh32T-xN*pfcJM>=Z^O5eC${ z23C=P>Kar4cN(bX3GhbMid_7IY8nKv1v24Uk?I|g30w@gs|kb&hk#5-rGc;$RBa#^ zw4iDO0iFPvARVwu1lhHqf*3j5g9>5YbK)R1Eyl++O5KF|nw6rm@) T0B=?{kWx+{R0XDvEHDoMzsJ<_ literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..3945fd020d3455db0bef268d12ae67809c90c2b7 GIT binary patch literal 1150 zcmWIWW@Zs#;Nak3u<&^2#()Gk8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gH`sg4aoHjkb7SsI^Urdhzdio^ zR`vknEVEB5L;_vcHn>WcnpjM;m?rtDT`S~ShvMy9d($@feQ_|FVx=`jeDVVQ$J!Ua zb8ci>=+x%+dYiG(_d{P@ElR4@!X9pzEAaL*yT6;;%Yri-ol5=vllBb!Q;M%t z%6zY0!Y;$w7s{R;k-lP%_xkRnoCy*|Nz%7y(GX8RxH={&pGt!ir!ua zbv6~VHa?z?gDQbi6OV5ZrSyRTh7ejpQ?{1NU2WdP(T0c8LLcm!mEwP0iiWQT(y j4>>$Qk%s_Tz@We|0~D13-mGjOWvoDG4onS(%pe{B`N?6@ literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..5600ed279efb99189791ed00b3bbb4203efaa208 GIT binary patch literal 3313 zcmc&$&u`pB6n@^_WV6nuNq;15Xxh2~q)k!0O}QYpMNPK}O_Q`y6M{s6%GsR+x4Rz8 z_NFZmp^5|Q2_a4hlmkM5I3U3R5E5MA)b@n<11fPqNN_+1aX}T|vmM*JF%=R*cr~_n z{NBv_@qO>j!fZCD67>*`oW6OM5?6t8G(WX8IX-i4x_s{z(L$T(z|Qwary$N_4@M+9 zKY4CudTMb=pP!yLEOS-3-uSX`TvJ+)YZLm>KDFuy!xQ!KQ`dRoG5zSJiDMI5>AYjq z8bYf|$Ci%aL4K@H?Nj4z4`1CpoiGsg5gE2!jwK$1#LuH2epJd3@#qUBrlr(t4a0SB z-TU%u%sonnRqCc)1=>w}dT3CkA=;a#eR&$r(*p{1A2%)2dqSaXX>3`cTvgVELj7~5 zCC;^)E5cbaRvMVeIxS0~q0+^<%f`AY{8pH8E|nY!4g3VhRmk zXjz`w6w9V-Vx39LlAiCdtI&}->8zDqn-^JijHb9MohxP66ONGHbz79HvS~|8SYD-w zv=#ESzepo=K%pJjK!ZURG<99i)4?JgB2A$@2MHe)8i>t2zj9gBJnWaY-HMbRZ`3rb zIy((}E#b83FJ6aDO@u%=_%Ij*z?9_$815kk!e~|$+U;-HJ`ID|JFkJz4VKa(M~A5k z>46|}mBAB!;trhB0X!8vyN-NL$|rbHs26t@#w6XKB9b16J`7c0fC`^dW>@wz%EjWU z|DnfUH$yuhl%aka2p8I!#5#lSi=Q?x3}XWxMF@rQ^s{LjwJXM&&@fj~O{B*0G=x;| z$e_+4)whN8zAACjSusqydNO)f?bb@etZ6>7HO@ZvZ-^$a;etjT+0$N93n$O7BVA@) zsD)5>U7B?UO(}paaHAQ{nmYz*EpY|e)}gG2xhsvf!)WM8>zNP8PLWrCy@zlf)V><3mpDW*vIK&?!hVC3;379J*H&9y$nQp!47^BG53(h zPVTXIz`Yz+?egn$4uU%{`tdD812HCU0~1ev@N463eBd#N(HXrq3i8KFJ1G)@4iO#V z-sOT@(|q_d3PSu}d@h8)_kts@x>^+d1Rp{!Ao4L661mHEN!8OyT}*w=!CutVwItTx&T( zpR)MOGl{usIOkX86VIxKZR^ugqTqMi@x+a4uW$&@%{p*nb$!8 literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar new file mode 100644 index 0000000000000000000000000000000000000000..4c2254f6352b5443de1396397423068a792b2eb5 GIT binary patch literal 1408 zcmWIWW@h1H0D(E)T7h5&l;C8LVeoYgan$wnbJGtE;bdSw{x~uogi9;985mio! z6LHmP3{$f|6@4sX1Um@gryeYRa`N{NLHKDo&;=lj#ZM3;`q7LK1o{bXL~>4IadB!f zBzV>VjaiS+s1`J%#IPBalA4gB1ED*-xG)QotshQ_&__&tA9vBl3fLI-;d5#4I zIho0cC7JnodSEw$W1wL2!5|PuvmiGh)|<&u#CGxO#jmF=%bdT=mgSl=_g*PSmy69I z30Iu`A6uO`@#fs7b%FM)<+!R|uzy*+Xt#8#6LYcsIos#w_J02O^Bdm-hFs}N9nnD7 zwGFA#r6v|(7GaW~+O+~c3OaAwTzxoU?u>;g8s9?{^q2i}aJ!zgfOBKbLZ&vi=j+VH zz8|`(B(d|aMbN<&vpL>gX7_K~Xmqe8x%bn)nQrrJf;)E<$+SgPDrvq~FJhNr?JH%M zzLBCa*L$t+?`tkqCoYz3Xcb%8cFFvmyr$#k?fUG=FOTwUmHc9TNM_@Ii_WF~+_N+< zzP|rh`*GaGH-@Xy zJS73l79b$N@YWGTBjqqwNDf1b4Ty2L@)yK7VB9k-X$0juxN#`C4QMhbw_!CIS4Kg0 z=oO&J@H8J90yGY5mO=O%Gt(d&Ck}Kk+&DxQ0vZd-LU>$>nSqdvO~Pg@G&=#!#+sEt qPR5m)U}l5Db`wssv1BcvWmvLRfHx}}FdP}!fKY;ofgy+)!~+16@N`rF literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx new file mode 100644 index 00000000000..b84b99a6b47 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx @@ -0,0 +1,5 @@ +- "BOOT-INF/layers/one/lib/a.jar" +- "BOOT-INF/layers/one/lib/b.jar" +- "BOOT-INF/layers/one/lib/c.jar" +- "BOOT-INF/layers/two/lib/d.jar" +- "BOOT-INF/layers/two/lib/e.jar" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..d95a13c5284 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Start-Class: ${foo.main} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties new file mode 100644 index 00000000000..32f7d00f2d0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties @@ -0,0 +1 @@ +foo.main: demo.FooApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml new file mode 100644 index 00000000000..cf04aa4fbe4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml @@ -0,0 +1,6 @@ + + + + diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle new file mode 100644 index 00000000000..7c4095f73b1 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle @@ -0,0 +1,44 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" + id "org.springframework.boot.integration-test" +} + +description = "Spring Boot Loader Integration Tests" + +configurations { + app +} + +dependencies { + app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository") + + intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + intTestImplementation("org.testcontainers:junit-jupiter") + intTestImplementation("org.testcontainers:testcontainers") +} + +task syncMavenRepository(type: Sync) { + from configurations.app + into "${buildDir}/int-test-maven-repository" +} + +task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-loader-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-loader-tests-app") +} + +task buildApp(type: GradleBuild) { + dependsOn syncAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-loader-tests-app" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + +intTest { + dependsOn buildApp +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle new file mode 100644 index 00000000000..37596c62063 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.webjars:jquery:3.5.0") +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle new file mode 100644 index 00000000000..06d9554ad0d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java new file mode 100644 index 00000000000..0c9d429350d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loaderapp; + +import java.io.File; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Arrays; + +import jakarta.servlet.ServletContext; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.util.FileCopyUtils; + +@SpringBootApplication +public class LoaderTestApplication { + + @Bean + public CommandLineRunner commandLineRunner(ServletContext servletContext) { + return (args) -> { + File temp = new File(System.getProperty("java.io.tmpdir")); + URL resourceUrl = servletContext.getResource("webjars/jquery/3.5.0/jquery.js"); + JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection(); + String jarName = connection.getJarFile().getName(); + System.out.println(">>>>> jar file " + jarName); + if(jarName.contains(temp.getAbsolutePath())) { + System.out.println(">>>>> jar written to temp"); + } + byte[] resourceContent = FileCopyUtils.copyToByteArray(resourceUrl.openStream()); + URL directUrl = new URL(resourceUrl.toExternalForm()); + byte[] directContent = FileCopyUtils.copyToByteArray(directUrl.openStream()); + String message = (!Arrays.equals(resourceContent, directContent)) ? "NO MATCH" + : directContent.length + " BYTES"; + System.out.println(">>>>> " + message + " from " + resourceUrl); + }; + } + + public static void main(String[] args) { + SpringApplication.run(LoaderTestApplication.class, args).close(); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java new file mode 100644 index 00000000000..a2d6db7c7fd --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import org.springframework.boot.system.JavaVersion; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests loader that supports fat jars. + * + * @author Phillip Webb + * @author Moritz Halbritter + */ +@DisabledIfDockerUnavailable +class LoaderIntegrationTests { + + private final ToStringConsumer output = new ToStringConsumer(); + + @ParameterizedTest + @MethodSource("javaRuntimes") + void readUrlsWithoutWarning(JavaRuntime javaRuntime) { + try (GenericContainer container = createContainer(javaRuntime)) { + container.start(); + System.out.println(this.output.toUtf8String()); + assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from") + .doesNotContain("WARNING:") + .doesNotContain("illegal") + .doesNotContain("jar written to temp"); + } + } + + private GenericContainer createContainer(JavaRuntime javaRuntime) { + return javaRuntime.getContainer() + .withLogConsumer(this.output) + .withCopyFileToContainer(MountableFile.forHostPath(findApplication().toPath()), "/app.jar") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))) + .withCommand("java", "-jar", "app.jar"); + } + + private File findApplication() { + String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app"); + File jar = new File(name); + Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?"); + return jar; + } + + static Stream javaRuntimes() { + List javaRuntimes = new ArrayList<>(); + javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.SEVENTEEN)); + javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.TWENTY)); + javaRuntimes.add(JavaRuntime.oracleJdk17()); + javaRuntimes.add(JavaRuntime.openJdkEarlyAccess(JavaVersion.TWENTY_ONE)); + return javaRuntimes.stream().filter(JavaRuntime::isCompatible); + } + + static final class JavaRuntime { + + private final String name; + + private final JavaVersion version; + + private final Supplier> container; + + private JavaRuntime(String name, JavaVersion version, Supplier> container) { + this.name = name; + this.version = version; + this.container = container; + } + + private boolean isCompatible() { + return this.version.isEqualOrNewerThan(JavaVersion.getJavaVersion()); + } + + GenericContainer getContainer() { + return this.container.get(); + } + + @Override + public String toString() { + return this.name; + } + + static JavaRuntime openJdkEarlyAccess(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("openjdk:%s-ea-jdk".formatted(imageVersion)); + return new JavaRuntime("OpenJDK Early Access " + imageVersion, version, + () -> new GenericContainer<>(image)); + } + + static JavaRuntime openJdk(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("bellsoft/liberica-openjdk-debian:" + imageVersion); + return new JavaRuntime("OpenJDK " + imageVersion, version, () -> new GenericContainer<>(image)); + } + + static JavaRuntime oracleJdk17() { + String arch = System.getProperty("os.arch"); + String dockerFile = ("aarch64".equals(arch)) ? "Dockerfile-aarch64" : "Dockerfile"; + ImageFromDockerfile image = new ImageFromDockerfile("spring-boot-loader/oracle-jdk-17") + .withFileFromFile("Dockerfile", new File("src/intTest/resources/conf/oracle-jdk-17/" + dockerFile)); + return new JavaRuntime("Oracle JDK 17", JavaVersion.SEVENTEEN, () -> new GenericContainer<>(image)); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile new file mode 100644 index 00000000000..2a50709dc5a --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:jammy-20230624 +RUN apt-get update && \ + apt-get install -y software-properties-common curl && \ + mkdir -p /opt/oraclejdk && \ + cd /opt/oraclejdk && \ + curl -L https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz | tar zx --strip-components=1 +ENV JAVA_HOME /opt/oraclejdk +ENV PATH $JAVA_HOME/bin:$PATH diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 new file mode 100644 index 00000000000..3f8614c7a21 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 @@ -0,0 +1,8 @@ +FROM ubuntu:jammy-20230624 +RUN apt-get update && \ + apt-get install -y software-properties-common curl && \ + mkdir -p /opt/oraclejdk && \ + cd /opt/oraclejdk && \ + curl -L https://download.oracle.com/java/17/archive/jdk-17.0.8_linux-aarch64_bin.tar.gz | tar zx --strip-components=1 +ENV JAVA_HOME /opt/oraclejdk +ENV PATH $JAVA_HOME/bin:$PATH diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc new file mode 100644 index 00000000000..28704af225f --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc @@ -0,0 +1,5 @@ +This folder contains a Dockerfile that will create an Oracle JDK instance for use in integration tests. +The resulting Docker image should not be published. + +Oracle JDK is subject to the https://www.oracle.com/downloads/licenses/no-fee-license.html["Oracle No-Fee Terms and Conditions" License (NFTC)] license. +We are specifically using the unmodified JDK for the purposes of developing and testing. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml new file mode 100644 index 00000000000..b8a41480d7d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml @@ -0,0 +1,4 @@ + + + + From 55b5610dd9d5929b55c60442b92a2cc0f8fb83b2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 23:12:19 -0700 Subject: [PATCH 3/3] Add Maven and Gradle option for the loader implementation to use Add properties to the Maven and Gradle plugins so that users can switch between the two loader modules. See gh-37669 --- .../gradle/tasks/bundling/BootArchive.java | 11 ++++ .../tasks/bundling/BootArchiveSupport.java | 13 +++-- .../boot/gradle/tasks/bundling/BootJar.java | 8 ++- .../boot/gradle/tasks/bundling/BootWar.java | 8 ++- .../tasks/bundling/BootZipCopyAction.java | 10 +++- .../tasks/bundling/LoaderZipEntries.java | 9 ++- .../AbstractBootArchiveIntegrationTests.java | 10 ++++ .../bundling/AbstractBootArchiveTests.java | 12 ++++ ...otJarIntegrationTests-classicLoader.gradle | 9 +++ ...otWarIntegrationTests-classicLoader.gradle | 9 +++ .../spring-boot-loader-tools/build.gradle | 24 +++++++- .../boot/loader/tools/AbstractJarWriter.java | 14 ++--- .../boot/loader/tools/Layouts.java | 2 +- .../loader/tools/LoaderClassesWriter.java | 10 +++- .../loader/tools/LoaderImplementation.java | 51 ++++++++++++++++ .../boot/loader/tools/Packager.java | 12 +++- .../boot/maven/JarIntegrationTests.java | 20 +++++++ .../projects/jar-with-classic-loader/pom.xml | 58 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 24 ++++++++ .../boot/maven/AbstractPackagerMojo.java | 11 ++++ .../boot/maven/BuildImageMojo.java | 13 +++++ .../boot/maven/RepackageMojo.java | 13 +++++ .../build.gradle | 4 ++ 23 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java index 1f3875a45aa..2da97a470c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java @@ -33,6 +33,8 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A Spring Boot "fat" archive task. * @@ -133,4 +135,13 @@ public interface BootArchive extends Task { */ void resolvedArtifacts(Provider> resolvedArtifacts); + /** + * The loader implementation that should be used with the archive. + * @return the loader implementation + * @since 3.2.0 + */ + @Input + @Optional + Property getLoaderImplementation(); + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 921e9f3d485..22b086248ac 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -42,6 +42,8 @@ import org.gradle.api.tasks.WorkResult; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * Support class for implementations of {@link BootArchive}. * @@ -116,12 +118,13 @@ class BootArchiveSupport { return (version != null) ? version : "unknown"; } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies) { - return createCopyAction(jar, resolvedDependencies, null, null); + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation) { + return createCopyAction(jar, resolvedDependencies, loaderImplementation, null, null); } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, - String layerToolsLocation) { + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation, LayerResolver layerResolver, String layerToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -136,7 +139,7 @@ class BootArchiveSupport { String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, - compressionResolver, encoding, resolvedDependencies, layerResolver); + compressionResolver, encoding, resolvedDependencies, layerResolver, loaderImplementation); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 5cf51bb8507..c76a95f1d6c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.bundling.Jar; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link Jar} task that produces a Spring Boot executable jar. * @@ -141,12 +143,14 @@ public abstract class BootJar extends Jar implements BootArchive { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index d697f00a1e5..d3aa0eab860 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.bundling.War; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link War} task that produces a Spring Boot executable war. * @@ -115,12 +117,14 @@ public abstract class BootWar extends War implements BootArchive { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 8e3a57e4357..1f35d482fb1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -57,6 +57,7 @@ import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.LayersIndex; import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.NativeImageArgFile; import org.springframework.boot.loader.tools.ReachabilityMetadataProperties; import org.springframework.util.Assert; @@ -111,11 +112,14 @@ class BootZipCopyAction implements CopyAction { private final LayerResolver layerResolver; + private final LoaderImplementation loaderImplementation; + BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode, boolean includeDefaultLoader, String layerToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, - ResolvedDependencies resolvedDependencies, LayerResolver layerResolver) { + ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, + LoaderImplementation loaderImplementation) { this.output = output; this.manifest = manifest; this.preserveFileTimestamps = preserveFileTimestamps; @@ -131,6 +135,7 @@ class BootZipCopyAction implements CopyAction { this.encoding = encoding; this.resolvedDependencies = resolvedDependencies; this.layerResolver = layerResolver; + this.loaderImplementation = loaderImplementation; } @Override @@ -310,7 +315,8 @@ class BootZipCopyAction implements CopyAction { // Always write loader entries after META-INF directory (see gh-16698) return; } - LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode()); + LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode(), + BootZipCopyAction.this.loaderImplementation); this.writtenLoaderEntries = loaderEntries.writeTo(this.out); if (BootZipCopyAction.this.layerResolver != null) { for (String name : this.writtenLoaderEntries.getFiles()) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 7606a3d66a2..8a7851f07c0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -28,6 +28,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StreamUtils; /** @@ -39,22 +40,26 @@ import org.springframework.util.StreamUtils; */ class LoaderZipEntries { + private final LoaderImplementation loaderImplementation; + private final Long entryTime; private final int dirMode; private final int fileMode; - LoaderZipEntries(Long entryTime, int dirMode, int fileMode) { + LoaderZipEntries(Long entryTime, int dirMode, int fileMode, LoaderImplementation loaderImplementation) { this.entryTime = entryTime; this.dirMode = dirMode; this.fileMode = fileMode; + this.loaderImplementation = (loaderImplementation != null) ? loaderImplementation + : LoaderImplementation.DEFAULT; } WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader-classic.jar"))) { + getClass().getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index a01d3bd4157..45c10a127db 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -106,6 +106,16 @@ abstract class AbstractBootArchiveIntegrationTests { assertThat(firstHash).isEqualTo(secondHash); } + @TestTemplate + void classicLoader() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + } + } + @TestTemplate void upToDateWhenBuiltTwice() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 17623f9ed41..3dffc007520 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -65,6 +65,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.gradle.junit.GradleProjectBuilder; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -279,6 +280,17 @@ abstract class AbstractBootArchiveTests { } } + @Test + void loaderIsWrittenToTheRootOfTheJarWhenUsingClassicLoader() throws IOException { + this.task.getMainClass().set("com.example.Main"); + this.task.getLoaderImplementation().set(LoaderImplementation.CLASSIC); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); + } + } + @Test void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { this.task.getMainClass().set("com.example.Main"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle new file mode 100644 index 00000000000..2e9e26c99ca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle new file mode 100644 index 00000000000..fd14cc1a64a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 80311be0acb..f7968f659d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -13,6 +13,10 @@ configurations { extendsFrom dependencyManagement transitive = false } + loaderClassic { + extendsFrom dependencyManagement + transitive = false + } jarmode { extendsFrom dependencyManagement transitive = false @@ -36,7 +40,8 @@ dependencies { compileOnly("ch.qos.logback:logback-classic") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loaderClassic(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) @@ -57,6 +62,21 @@ task reproducibleLoaderJar(type: Jar) { } reproducibleFileOrder = true preserveFileTimestamps = false + archiveFileName = "spring-boot-loader.jar" + destinationDirectory = file("${generatedResources}/META-INF/loader") +} + +task reproducibleLoaderClassicJar(type: Jar) { + dependsOn configurations.loaderClassic + from { + zipTree(configurations.loaderClassic.incoming.files.singleFile).matching { + exclude "META-INF/LICENSE.txt" + exclude "META-INF/NOTICE.txt" + exclude "META-INF/spring-boot.properties" + } + } + reproducibleFileOrder = true + preserveFileTimestamps = false archiveFileName = "spring-boot-loader-classic.jar" destinationDirectory = file("${generatedResources}/META-INF/loader") } @@ -72,7 +92,7 @@ task layerToolsJar(type: Sync) { sourceSets { main { - output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar]) + output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 23282dbea72..7e807b1f9d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -51,8 +51,6 @@ import org.apache.commons.compress.archivers.zip.UnixStat; */ public abstract class AbstractJarWriter implements LoaderClassesWriter { - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader-classic.jar"; - private static final int BUFFER_SIZE = 32 * 1024; private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; @@ -199,13 +197,15 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter { return library.getLastModified(); } - /** - * Write the required spring-boot-loader classes to the JAR. - * @throws IOException if the classes cannot be written - */ @Override public void writeLoaderClasses() throws IOException { - writeLoaderClasses(NESTED_LOADER_JAR); + writeLoaderClasses(LoaderImplementation.DEFAULT); + } + + @Override + public void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException { + writeLoaderClasses((loaderImplementation != null) ? loaderImplementation.getJarResourceName() + : LoaderImplementation.DEFAULT.getJarResourceName()); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 82c73ef8598..e6f99282a71 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -23,7 +23,7 @@ import java.util.Locale; import java.util.Map; /** - * Common {@link Layout}s. + * Common {@link Layout layouts}. * * @author Phillip Webb * @author Dave Syer diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java index 187ff0b9029..864992279d3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,14 @@ public interface LoaderClassesWriter { */ void writeLoaderClasses() throws IOException; + /** + * Write the default required spring-boot-loader classes to the JAR. + * @param loaderImplementation the specific implementation to write + * @throws IOException if the classes cannot be written + * @since 3.2.0 + */ + void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException; + /** * Write custom required spring-boot-loader classes to the JAR. * @param loaderJarResourceName the name of the resource containing the loader classes diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java new file mode 100644 index 00000000000..6414a3cfbbf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.loader.tools; + +/** + * Supported loader implementations. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public enum LoaderImplementation { + + /** + * The default recommended loader implementation. + */ + DEFAULT("META-INF/loader/spring-boot-loader.jar"), + + /** + * The classic loader implementation as used with Spring Boot 3.1 and earlier. + */ + CLASSIC("META-INF/loader/spring-boot-loader-classic.jar"); + + private final String jarResourceName; + + LoaderImplementation(String jarResourceName) { + this.jarResourceName = jarResourceName; + } + + /** + * Return the name of the nested resource that can be loaded from the tools jar. + * @return the jar resource name + */ + public String getJarResourceName() { + return this.jarResourceName; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 815e0ee4a93..af4dff23301 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -88,6 +88,8 @@ public abstract class Packager { private Layout layout; + private LoaderImplementation loaderImplementation; + private LayoutFactory layoutFactory; private Layers layers; @@ -135,6 +137,14 @@ public abstract class Packager { this.layout = layout; } + /** + * Sets the loader implementation to use. + * @param loaderImplementation the loaderImplementation to set + */ + public void setLoaderImplementation(LoaderImplementation loaderImplementation) { + this.loaderImplementation = loaderImplementation; + } + /** * Sets the layout factory for the jar. The factory can be used when no specific * layout is specified. @@ -215,7 +225,7 @@ public abstract class Packager { customLoaderLayout.writeLoadedClasses(writer); } else if (layout.isExecutable()) { - writer.writeLoaderClasses(); + writer.writeLoaderClasses(this.loaderImplementation); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 1d37949cfaa..c903da23a2c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -75,6 +75,26 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void whenJarWithClassicLoaderIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-classic-loader").goals("install").execute((project) -> { + File original = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(launchScript(repackaged)).isEmpty(); + assertThat(jar(repackaged)).manifest((manifest) -> { + manifest.hasMainClass("org.springframework.boot.loader.launch.JarLauncher"); + manifest.hasStartClass("some.random.Main"); + manifest.hasAttribute("Not-Used", "Foo"); + }).hasEntryWithName("org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(buildLog(project)) + .contains("Replacing main artifact " + repackaged + " with repackaged archive,") + .contains("The original artifact has been renamed to " + original) + .contains("Installing " + repackaged + " to") + .doesNotContain("Installing " + original + " to"); + }); + } + @TestTemplate void whenAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) { mavenBuild.project("jar-attach-disabled").goals("install").execute((project) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml new file mode 100644 index 00000000000..64d9d04f994 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-classic-loader + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + Foo + + + CLASSIC + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..5e51546d4e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index b6dfdc04bb0..28d55d213a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -47,6 +47,7 @@ import org.springframework.boot.loader.tools.Layouts.Jar; import org.springframework.boot.loader.tools.Layouts.None; import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Packager; import org.springframework.boot.loader.tools.layer.CustomLayers; @@ -128,6 +129,15 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo return null; } + /** + * Return the loader implementation that should be used. + * @return the loader implementation or {@code null} + * @since 3.2.0 + */ + protected LoaderImplementation getLoaderImplementation() { + return null; + } + /** * Return the layout factory that will be used to determine the {@link LayoutType} if * no explicit layout is set. @@ -145,6 +155,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo */ protected

P getConfiguredPackager(Supplier

supplier) { P packager = supplier.get(); + packager.setLoaderImplementation(getLoaderImplementation()); packager.setLayoutFactory(getLayoutFactory()); packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog)); packager.setMainClass(this.mainClass); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 84589c01891..79b62bf5303 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -48,6 +48,7 @@ import org.springframework.boot.loader.tools.EntryWriter; import org.springframework.boot.loader.tools.ImagePackager; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StringUtils; /** @@ -187,6 +188,13 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { @Parameter private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -206,6 +214,11 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 5ca2ecac5a9..13a16c2a144 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -36,6 +36,7 @@ import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Repackager; /** @@ -161,6 +162,13 @@ public class RepackageMojo extends AbstractPackagerMojo { @Parameter(property = "spring-boot.repackage.layout") private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -180,6 +188,11 @@ public class RepackageMojo extends AbstractPackagerMojo { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle index 37596c62063..16f7a57ebe5 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle @@ -16,3 +16,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.webjars:jquery:3.5.0") } + +bootJar { + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} \ No newline at end of file