diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle index 3103d3a44fd..144ea7617d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -45,5 +45,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle index 131fb7d18ce..c2aed983d37 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -28,5 +28,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle index f79bdd4414a..00e20588ebb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle @@ -31,5 +31,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle index eae79719225..f1db5f69c20 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle @@ -62,5 +62,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle index 16986fb0668..e7e289c9b9b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle @@ -40,5 +40,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle index a643858e224..428ba655b7c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle @@ -46,5 +46,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle index 07cda46020a..c5057199662 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle @@ -29,5 +29,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle index 20daaf0f3aa..9570b55f627 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle @@ -32,5 +32,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle index c798b6bf190..d488653c693 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle @@ -63,5 +63,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle index 8f467a84b68..227103d4374 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle @@ -41,5 +41,5 @@ task listLayers(type: JavaExec) { task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files systemProperties = [ "jarmode": "tools" ] - args "extract", "--layers", "--launcher" + args "extract", "--layers", "--launcher", "--destination", ".", "--force" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index dace9d19437..4821d686b4c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -27,6 +27,7 @@ import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; @@ -52,7 +53,7 @@ class ExtractCommand extends Command { /** * Option to create a launcher. */ - static final Option LAUNCHER_OPTION = Option.of("launcher", null, "Whether to extract the Spring Boot launcher"); + static final Option LAUNCHER_OPTION = Option.flag("launcher", "Whether to extract the Spring Boot launcher"); /** * Option to extract layers. @@ -63,13 +64,18 @@ class ExtractCommand extends Command { * Option to specify the destination to write to. */ static final Option DESTINATION_OPTION = Option.of("destination", "string", - "Directory to extract files to. Defaults to the current working directory"); + "Directory to extract files to. Defaults to a directory named after the uber JAR (without the file extension)"); + + /** + * Option to ignore non-empty directory error. + */ + static final Option FORCE_OPTION = Option.flag("force", "Whether to ignore non-empty directories, extract anyway"); private static final Option LIBRARIES_DIRECTORY_OPTION = Option.of("libraries", "string", "Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/"); - private static final Option RUNNER_FILENAME_OPTION = Option.of("runner-filename", "string", - "Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar"); + private static final Option APPLICATION_FILENAME_OPTION = Option.of("application-filename", "string", + "Name of the application JAR file. Only applicable when not using --launcher. Defaults to the uber JAR filename"); private final Context context; @@ -81,7 +87,8 @@ class ExtractCommand extends Command { ExtractCommand(Context context, Layers layers) { super("extract", "Extract the contents from the jar", Options.of(LAUNCHER_OPTION, LAYERS_OPTION, - DESTINATION_OPTION, LIBRARIES_DIRECTORY_OPTION, RUNNER_FILENAME_OPTION), Parameters.none()); + DESTINATION_OPTION, LIBRARIES_DIRECTORY_OPTION, APPLICATION_FILENAME_OPTION, FORCE_OPTION), + Parameters.none()); this.context = context; this.layers = layers; } @@ -90,7 +97,8 @@ class ExtractCommand extends Command { void run(PrintStream out, Map options, List parameters) { try { checkJarCompatibility(); - File destination = getWorkingDirectory(options); + File destination = getDestination(options); + checkDirectoryIsEmpty(options, destination); FileResolver fileResolver = getFileResolver(destination, options); fileResolver.createDirectories(); if (options.containsKey(LAUNCHER_OPTION)) { @@ -99,7 +107,7 @@ class ExtractCommand extends Command { else { JarStructure jarStructure = getJarStructure(); extractLibraries(fileResolver, jarStructure, options); - createRunner(jarStructure, fileResolver, options); + createApplication(jarStructure, fileResolver, options); } } catch (IOException ex) { @@ -108,15 +116,36 @@ class ExtractCommand extends Command { catch (LayersNotEnabledException ex) { printError(out, "Layers are not enabled"); } + catch (AbortException ex) { + printError(out, ex.getMessage()); + } + } + + private static void checkDirectoryIsEmpty(Map options, File destination) { + if (options.containsKey(FORCE_OPTION)) { + return; + } + if (!destination.exists()) { + return; + } + if (!destination.isDirectory()) { + throw new AbortException(destination.getAbsoluteFile() + " already exists and is not a directory"); + } + File[] files = destination.listFiles(); + if (files != null && files.length > 0) { + throw new AbortException(destination.getAbsoluteFile() + " already exists and is not empty"); + } } private void checkJarCompatibility() throws IOException { File file = this.context.getArchiveFile(); try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) { ZipEntry entry = stream.getNextEntry(); - Assert.state(entry != null, - () -> "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled" - .formatted(file)); + if (entry == null) { + throw new AbortException( + "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled" + .formatted(file)); + } } } @@ -149,20 +178,31 @@ class ExtractCommand extends Command { } private FileResolver getFileResolver(File destination, Map options) { - String runnerFilename = getRunnerFilename(options); + String applicationFilename = getApplicationFilename(options); if (!options.containsKey(LAYERS_OPTION)) { - return new NoLayersFileResolver(destination, runnerFilename); + return new NoLayersFileResolver(destination, applicationFilename); } Layers layers = getLayers(); Set layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); - return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename); + return new LayersFileResolver(destination, layers, layersToExtract, applicationFilename); } - private File getWorkingDirectory(Map options) { + private File getDestination(Map options) { if (options.containsKey(DESTINATION_OPTION)) { - return new File(options.get(DESTINATION_OPTION)); + File destination = new File(options.get(DESTINATION_OPTION)); + if (destination.isAbsolute()) { + return destination; + } + return new File(this.context.getWorkingDir(), destination.getPath()); } - return this.context.getWorkingDir(); + return new File(this.context.getWorkingDir(), stripExtension(this.context.getArchiveFile().getName())); + } + + private static String stripExtension(String name) { + if (name.toLowerCase(Locale.ROOT).endsWith(".jar") || name.toLowerCase(Locale.ROOT).endsWith(".war")) { + return name.substring(0, name.length() - 4); + } + return name; } private JarStructure getJarStructure() { @@ -199,9 +239,9 @@ class ExtractCommand extends Command { return Layers.get(this.context); } - private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map options) + private void createApplication(JarStructure jarStructure, FileResolver fileResolver, Map options) throws IOException { - File file = fileResolver.resolveRunner(); + File file = fileResolver.resolveApplication(); if (file == null) { return; } @@ -221,11 +261,11 @@ class ExtractCommand extends Command { } } - private String getRunnerFilename(Map options) { - if (options.containsKey(RUNNER_FILENAME_OPTION)) { - return options.get(RUNNER_FILENAME_OPTION); + private String getApplicationFilename(Map options) { + if (options.containsKey(APPLICATION_FILENAME_OPTION)) { + return options.get(APPLICATION_FILENAME_OPTION); } - return "runner.jar"; + return this.context.getArchiveFile().getName(); } private static boolean isType(Entry entry, Type type) { @@ -338,11 +378,12 @@ class ExtractCommand extends Command { File resolve(String originalName, String newName) throws IOException; /** - * Resolves the file for the runner. - * @return the file for the runner or {@code null} if the runner should be skipped + * Resolves the file for the application. + * @return the file for the application or {@code null} if the application should + * be skipped * @throws IOException if something went wrong */ - File resolveRunner() throws IOException; + File resolveApplication() throws IOException; } @@ -350,11 +391,11 @@ class ExtractCommand extends Command { private final File directory; - private final String runnerFilename; + private final String applicationFilename; - private NoLayersFileResolver(File directory, String runnerFilename) { + private NoLayersFileResolver(File directory, String applicationFilename) { this.directory = directory; - this.runnerFilename = runnerFilename; + this.applicationFilename = applicationFilename; } @Override @@ -367,8 +408,8 @@ class ExtractCommand extends Command { } @Override - public File resolveRunner() throws IOException { - return resolve(this.runnerFilename, this.runnerFilename); + public File resolveApplication() throws IOException { + return resolve(this.applicationFilename, this.applicationFilename); } } @@ -381,13 +422,13 @@ class ExtractCommand extends Command { private final File directory; - private final String runnerFilename; + private final String applicationFilename; - LayersFileResolver(File directory, Layers layers, Set layersToExtract, String runnerFilename) { + LayersFileResolver(File directory, Layers layers, Set layersToExtract, String applicationFilename) { this.layers = layers; this.layersToExtract = layersToExtract; this.directory = directory; - this.runnerFilename = runnerFilename; + this.applicationFilename = applicationFilename; } @Override @@ -410,12 +451,12 @@ class ExtractCommand extends Command { } @Override - public File resolveRunner() throws IOException { + public File resolveApplication() throws IOException { String layer = this.layers.getApplicationLayerName(); if (shouldExtractLayer(layer)) { File directory = getLayerDirectory(layer); - return assertFileIsContainedInDirectory(directory, new File(directory, this.runnerFilename), - this.runnerFilename); + return assertFileIsContainedInDirectory(directory, new File(directory, this.applicationFilename), + this.applicationFilename); } return null; } @@ -433,4 +474,12 @@ class ExtractCommand extends Command { } + private static final class AbortException extends RuntimeException { + + AbortException(String message) { + super(message); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java index b2770b9fe41..d0db630a008 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java @@ -58,11 +58,10 @@ class ExtractLayersCommand extends Command { @Override void run(PrintStream out, Map options, List parameters) { Map rewrittenOptions = new HashMap<>(); - if (options.containsKey(DESTINATION_OPTION)) { - rewrittenOptions.put(ExtractCommand.DESTINATION_OPTION, options.get(DESTINATION_OPTION)); - } + rewrittenOptions.put(ExtractCommand.DESTINATION_OPTION, options.getOrDefault(DESTINATION_OPTION, ".")); rewrittenOptions.put(ExtractCommand.LAYERS_OPTION, StringUtils.collectionToCommaDelimitedString(parameters)); rewrittenOptions.put(ExtractCommand.LAUNCHER_OPTION, null); + rewrittenOptions.put(ExtractCommand.FORCE_OPTION, null); this.delegate.run(out, rewrittenOptions, Collections.emptyList()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java index e6a50f36333..c706764bb16 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -119,46 +119,46 @@ class ExtractCommandTests extends AbstractTests { class Extract { @Test - void extractLibrariesAndCreatesRunner() throws IOException { + void extractLibrariesAndCreatesApplication() throws IOException { run(ExtractCommandTests.this.archive); List filenames = listFilenames(); - assertThat(filenames).contains("lib/dependency-1.jar") - .contains("lib/dependency-2.jar") - .contains("lib/dependency-3-SNAPSHOT.jar") - .contains("runner.jar") - .doesNotContain("org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(filenames).contains("test/lib/dependency-1.jar") + .contains("test/lib/dependency-2.jar") + .contains("test/lib/dependency-3-SNAPSHOT.jar") + .contains("test/test.jar") + .doesNotContain("test/org/springframework/boot/loader/launch/JarLauncher.class"); } @Test - void extractLibrariesAndCreatesRunnerInDestination() throws IOException { + void extractLibrariesAndCreatesApplicationInDestination() throws IOException { run(ExtractCommandTests.this.archive, "--destination", file("out").getAbsolutePath()); List filenames = listFilenames(); assertThat(filenames).contains("out/lib/dependency-1.jar") .contains("out/lib/dependency-2.jar") .contains("out/lib/dependency-3-SNAPSHOT.jar") - .contains("out/runner.jar"); + .contains("out/test.jar"); } @Test - void runnerNameAndLibrariesDirectoriesCanBeCustomized() throws IOException { - run(ExtractCommandTests.this.archive, "--runner-filename", "runner-customized.jar", "--libraries", + void applicationNameAndLibrariesDirectoriesCanBeCustomized() throws IOException { + run(ExtractCommandTests.this.archive, "--application-filename", "application-customized.jar", "--libraries", "dependencies"); List filenames = listFilenames(); - assertThat(filenames).contains("dependencies/dependency-1.jar") - .contains("dependencies/dependency-2.jar") - .contains("dependencies/dependency-3-SNAPSHOT.jar"); - File runner = file("runner-customized.jar"); - assertThat(runner).exists(); - Map attributes = getJarManifestAttributes(runner); + assertThat(filenames).contains("test/dependencies/dependency-1.jar") + .contains("test/dependencies/dependency-2.jar") + .contains("test/dependencies/dependency-3-SNAPSHOT.jar"); + File application = file("test/application-customized.jar"); + assertThat(application).exists(); + Map attributes = getJarManifestAttributes(application); assertThat(attributes).containsEntry("Class-Path", "dependencies/dependency-1.jar dependencies/dependency-2.jar dependencies/dependency-3-SNAPSHOT.jar"); } @Test - void runnerContainsManifestEntries() throws IOException { + void applicationContainsManifestEntries() throws IOException { run(ExtractCommandTests.this.archive); - File runner = file("runner.jar"); - Map attributes = getJarManifestAttributes(runner); + File application = file("test/test.jar"); + Map attributes = getJarManifestAttributes(application); assertThat(attributes).containsEntry("Main-Class", "org.example.Main") .containsEntry("Class-Path", "lib/dependency-1.jar lib/dependency-2.jar lib/dependency-3-SNAPSHOT.jar") .containsEntry("Some-Attribute", "Some-Value") @@ -167,27 +167,27 @@ class ExtractCommandTests extends AbstractTests { } @Test - void runnerContainsApplicationClassesAndResources() throws IOException { + void applicationContainsApplicationClassesAndResources() throws IOException { run(ExtractCommandTests.this.archive); - File runner = file("runner.jar"); - List entryNames = getJarEntryNames(runner); + File application = file("test/test.jar"); + List entryNames = getJarEntryNames(application); assertThat(entryNames).contains("application.properties"); } @Test void appliesFileTimes() { run(ExtractCommandTests.this.archive); - assertThat(file("lib/dependency-1.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes); - assertThat(file("lib/dependency-2.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes); - assertThat(file("lib/dependency-3-SNAPSHOT.jar")).exists() + assertThat(file("test/lib/dependency-1.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes); + assertThat(file("test/lib/dependency-2.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes); + assertThat(file("test/lib/dependency-3-SNAPSHOT.jar")).exists() .satisfies(ExtractCommandTests.this::timeAttributes); } @Test - void runnerDoesntContainLibraries() throws IOException { + void applicationDoesntContainLibraries() throws IOException { run(ExtractCommandTests.this.archive); - File runner = file("runner.jar"); - List entryNames = getJarEntryNames(runner); + File application = file("test/test.jar"); + List entryNames = getJarEntryNames(application); assertThat(entryNames).doesNotContain("BOOT-INF/lib/dependency-1.jar", "BOOT-INF/lib/dependency-2.jar"); } @@ -197,7 +197,42 @@ class ExtractCommandTests extends AbstractTests { try (FileWriter writer = new FileWriter(file)) { writer.write("text"); } - assertThatIllegalStateException().isThrownBy(() -> run(file)).withMessageContaining("not compatible"); + TestPrintStream out = run(file); + assertThat(out).contains("is not compatible; ensure jar file is valid and launch script is not enabled"); + } + + @Test + void shouldFailIfDirectoryIsNotEmpty() throws IOException { + File destination = file("out"); + Files.createDirectories(destination.toPath()); + Files.createFile(new File(destination, "file.txt").toPath()); + TestPrintStream out = run(ExtractCommandTests.this.archive, "--destination", destination.getAbsolutePath()); + assertThat(out).contains("already exists and is not empty"); + } + + @Test + void shouldNotFailIfDirectoryExistsButIsEmpty() throws IOException { + File destination = file("out"); + Files.createDirectories(destination.toPath()); + run(ExtractCommandTests.this.archive, "--destination", destination.getAbsolutePath()); + List filenames = listFilenames(); + assertThat(filenames).contains("out/lib/dependency-1.jar") + .contains("out/lib/dependency-2.jar") + .contains("out/lib/dependency-3-SNAPSHOT.jar") + .contains("out/test.jar"); + } + + @Test + void shouldNotFailIfDirectoryIsNotEmptyButForceIsPassed() throws IOException { + File destination = file("out"); + Files.createDirectories(destination.toPath()); + Files.createFile(new File(destination, "file.txt").toPath()); + run(ExtractCommandTests.this.archive, "--destination", destination.getAbsolutePath(), "--force"); + List filenames = listFilenames(); + assertThat(filenames).contains("out/lib/dependency-1.jar") + .contains("out/lib/dependency-2.jar") + .contains("out/lib/dependency-3-SNAPSHOT.jar") + .contains("out/test.jar"); } } @@ -206,23 +241,23 @@ class ExtractCommandTests extends AbstractTests { class ExtractWithLayers { @Test - void extractLibrariesAndCreatesRunner() throws IOException { + void extractLibrariesAndCreatesApplication() throws IOException { run(ExtractCommandTests.this.archive, "--layers"); List filenames = listFilenames(); - assertThat(filenames).contains("dependencies/lib/dependency-1.jar") - .contains("dependencies/lib/dependency-2.jar") - .contains("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar") - .contains("application/runner.jar"); + assertThat(filenames).contains("test/dependencies/lib/dependency-1.jar") + .contains("test/dependencies/lib/dependency-2.jar") + .contains("test/snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar") + .contains("test/application/test.jar"); } @Test void extractsOnlySelectedLayers() throws IOException { run(ExtractCommandTests.this.archive, "--layers", "dependencies"); List filenames = listFilenames(); - assertThat(filenames).contains("dependencies/lib/dependency-1.jar") - .contains("dependencies/lib/dependency-2.jar") - .doesNotContain("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar") - .doesNotContain("application/runner.jar"); + assertThat(filenames).contains("test/dependencies/lib/dependency-1.jar") + .contains("test/dependencies/lib/dependency-2.jar") + .doesNotContain("test/snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar") + .doesNotContain("test/application/test.jar"); } @Test @@ -241,14 +276,14 @@ class ExtractCommandTests extends AbstractTests { void extract() throws IOException { run(ExtractCommandTests.this.archive, "--launcher"); List filenames = listFilenames(); - assertThat(filenames).contains("META-INF/MANIFEST.MF") - .contains("BOOT-INF/classpath.idx") - .contains("BOOT-INF/layers.idx") - .contains("BOOT-INF/lib/dependency-1.jar") - .contains("BOOT-INF/lib/dependency-2.jar") - .contains("BOOT-INF/lib/dependency-3-SNAPSHOT.jar") - .contains("BOOT-INF/classes/application.properties") - .contains("org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(filenames).contains("test/META-INF/MANIFEST.MF") + .contains("test/BOOT-INF/classpath.idx") + .contains("test/BOOT-INF/layers.idx") + .contains("test/BOOT-INF/lib/dependency-1.jar") + .contains("test/BOOT-INF/lib/dependency-2.jar") + .contains("test/BOOT-INF/lib/dependency-3-SNAPSHOT.jar") + .contains("test/BOOT-INF/classes/application.properties") + .contains("test/org/springframework/boot/loader/launch/JarLauncher.class"); } @Test @@ -267,14 +302,14 @@ class ExtractCommandTests extends AbstractTests { void extract() throws IOException { run(ExtractCommandTests.this.archive, "--launcher", "--layers"); List filenames = listFilenames(); - assertThat(filenames).contains("application/META-INF/MANIFEST.MF") - .contains("application/BOOT-INF/classpath.idx") - .contains("application/BOOT-INF/layers.idx") - .contains("dependencies/BOOT-INF/lib/dependency-1.jar") - .contains("dependencies/BOOT-INF/lib/dependency-2.jar") - .contains("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar") - .contains("application/BOOT-INF/classes/application.properties") - .contains("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(filenames).contains("test/application/META-INF/MANIFEST.MF") + .contains("test/application/BOOT-INF/classpath.idx") + .contains("test/application/BOOT-INF/layers.idx") + .contains("test/dependencies/BOOT-INF/lib/dependency-1.jar") + .contains("test/dependencies/BOOT-INF/lib/dependency-2.jar") + .contains("test/snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar") + .contains("test/application/BOOT-INF/classes/application.properties") + .contains("test/spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); } @Test @@ -288,14 +323,14 @@ class ExtractCommandTests extends AbstractTests { void extractsOnlySelectedLayers() throws IOException { run(ExtractCommandTests.this.archive, "--launcher", "--layers", "dependencies"); List filenames = listFilenames(); - assertThat(filenames).doesNotContain("application/META-INF/MANIFEST.MF") - .doesNotContain("application/BOOT-INF/classpath.idx") - .doesNotContain("application/BOOT-INF/layers.idx") - .contains("dependencies/BOOT-INF/lib/dependency-1.jar") - .contains("dependencies/BOOT-INF/lib/dependency-2.jar") - .doesNotContain("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar") - .doesNotContain("application/BOOT-INF/classes/application.properties") - .doesNotContain("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(filenames).doesNotContain("test/application/META-INF/MANIFEST.MF") + .doesNotContain("test/application/BOOT-INF/classpath.idx") + .doesNotContain("test/application/BOOT-INF/layers.idx") + .contains("test/dependencies/BOOT-INF/lib/dependency-1.jar") + .contains("test/dependencies/BOOT-INF/lib/dependency-2.jar") + .doesNotContain("test/snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar") + .doesNotContain("test/application/BOOT-INF/classes/application.properties") + .doesNotContain("test/spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index bf77b76a054..ca22d6cb1e3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -172,9 +172,10 @@ class ExtractLayersCommandTests { writer.write("text"); } given(this.context.getArchiveFile()).willReturn(file); - assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList())) - .withMessageContaining("not compatible"); + try (TestPrintStream out = new TestPrintStream(this)) { + this.command.run(out, Collections.emptyMap(), Collections.emptyList()); + assertThat(out).contains("is not compatible"); + } } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java index ce4bd67b8a4..07743ea9fce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java @@ -62,15 +62,25 @@ class TestPrintStream extends PrintStream implements AssertProvider