diff --git a/settings.gradle b/settings.gradle index 8312af44273..4c07940fbdd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -57,7 +57,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadat include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor" 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-jarmode-tools" 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" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 04f7a42dc9e..867434fe3dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1612,7 +1612,7 @@ bom { "spring-boot-configuration-processor", "spring-boot-devtools", "spring-boot-docker-compose", - "spring-boot-jarmode-layertools", + "spring-boot-jarmode-tools", "spring-boot-loader", "spring-boot-loader-classic", "spring-boot-loader-tools", 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 2da97a470c8..ec7d7099bcf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.boot.loader.tools.LoaderImplementation; * A Spring Boot "fat" archive task. * * @author Andy Wilkinson + * @author Moritz Halbritter * @since 2.0.0 */ public interface BootArchive extends Task { @@ -144,4 +145,13 @@ public interface BootArchive extends Task { @Optional Property getLoaderImplementation(); + /** + * Returns whether the JAR tools should be included as a dependency in the layered + * archive. + * @return whether the JAR tools should be included + * @since 3.3.0 + */ + @Input + Property getIncludeTools(); + } 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 330bc1aef1c..23cf1141756 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -129,7 +129,7 @@ class BootArchiveSupport { CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, LoaderImplementation loaderImplementation, boolean supportsSignatureFile, LayerResolver layerResolver, - String layerToolsLocation) { + String jarmodeToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -143,7 +143,7 @@ class BootArchiveSupport { Function compressionResolver = this.compressionResolver; String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, - includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, + includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, 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 7ed3f998c54..e7b542bebe3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,6 +88,7 @@ public abstract class BootJar extends Jar implements BootArchive { this.projectName = project.provider(project::getName); this.projectVersion = project.provider(project::getVersion); this.resolvedDependencies = new ResolvedDependencies(project); + getIncludeTools().convention(true); } private void configureBootInfSpec(CopySpec bootInfSpec) { @@ -144,13 +145,21 @@ public abstract class BootJar extends Jar implements BootArchive { @Override protected CopyAction createCopyAction() { LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); + LayerResolver layerResolver = null; 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, loaderImplementation, true, - layerResolver, layerToolsLocation); + layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); } - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true); + String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null; + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true, layerResolver, + jarmodeToolsLocation); + } + + @SuppressWarnings("removal") + private boolean isIncludeJarmodeTools() { + if (!this.getIncludeTools().get()) { + return false; + } + return this.layered.getIncludeLayerTools().get(); } @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 d19f152f84b..302e9ceb1be 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,6 +87,7 @@ public abstract class BootWar extends War implements BootArchive { this.projectName = project.provider(project::getName); this.projectVersion = project.provider(project::getVersion); this.resolvedDependencies = new ResolvedDependencies(project); + getIncludeTools().convention(true); } private Object getProvidedLibFiles() { @@ -118,13 +119,21 @@ public abstract class BootWar extends War implements BootArchive { @Override protected CopyAction createCopyAction() { LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); + LayerResolver layerResolver = null; 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, loaderImplementation, false, - layerResolver, layerToolsLocation); + layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); } - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false); + String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null; + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false, + layerResolver, jarmodeToolsLocation); + } + + @SuppressWarnings("removal") + private boolean isIncludeJarmodeTools() { + if (!this.getIncludeTools().get()) { + return false; + } + return this.layered.getIncludeLayerTools().get(); } @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 780944145dc..60bcebc0492 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 @@ -96,7 +96,7 @@ class BootZipCopyAction implements CopyAction { private final boolean includeDefaultLoader; - private final String layerToolsLocation; + private final String jarmodeToolsLocation; private final Spec requiresUnpack; @@ -119,7 +119,7 @@ class BootZipCopyAction implements CopyAction { private final LoaderImplementation loaderImplementation; BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode, - boolean includeDefaultLoader, String layerToolsLocation, Spec requiresUnpack, + boolean includeDefaultLoader, String jarmodeToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, LayerResolver layerResolver, @@ -130,7 +130,7 @@ class BootZipCopyAction implements CopyAction { this.dirMode = dirMode; this.fileMode = fileMode; this.includeDefaultLoader = includeDefaultLoader; - this.layerToolsLocation = layerToolsLocation; + this.jarmodeToolsLocation = jarmodeToolsLocation; this.requiresUnpack = requiresUnpack; this.exclusions = exclusions; this.launchScript = launchScript; @@ -342,8 +342,8 @@ class BootZipCopyAction implements CopyAction { } private void writeJarToolsIfNecessary() throws IOException { - if (BootZipCopyAction.this.layerToolsLocation != null) { - writeJarModeLibrary(BootZipCopyAction.this.layerToolsLocation, JarModeLibrary.LAYER_TOOLS); + if (BootZipCopyAction.this.jarmodeToolsLocation != null) { + writeJarModeLibrary(BootZipCopyAction.this.jarmodeToolsLocation, JarModeLibrary.TOOLS); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java index b2f6842f21f..98898783a26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,8 +72,10 @@ public abstract class LayeredSpec { * archive. * @return whether the layer tools should be included * @since 3.0.0 + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of {@code includeTools}. */ @Input + @Deprecated(since = "3.3.0", forRemoval = true) public abstract Property getIncludeLayerTools(); /** 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 4019ffbf6af..f7b07b990c5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Andy Wilkinson * @author Madhura Bhave * @author Scott Frederick + * @author Moritz Halbritter */ abstract class AbstractBootArchiveIntegrationTests { @@ -332,6 +333,18 @@ abstract class AbstractBootArchiveIntegrationTests { .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } + @TestTemplate + void notUpToDateWhenBuiltWithToolsAndThenWithoutTools() { + assertThat(this.gradleBuild.scriptProperty("includeTools", "") + .build(this.taskName) + .task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("includeTools", "includeTools = false") + .build(this.taskName) + .task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + @TestTemplate void layersWithCustomSourceSet() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) @@ -345,7 +358,7 @@ abstract class AbstractBootArchiveIntegrationTests { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); @@ -397,7 +410,7 @@ abstract class AbstractBootArchiveIntegrationTests { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); @@ -443,7 +456,7 @@ abstract class AbstractBootArchiveIntegrationTests { BuildResult build = this.gradleBuild.build(this.taskName); assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); @@ -490,7 +503,7 @@ abstract class AbstractBootArchiveIntegrationTests { BuildResult build = this.gradleBuild.build(this.taskName); assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); 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 c5c78eb5a21..ebda757e698 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,6 +80,7 @@ import static org.mockito.Mockito.mock; * @param the type of the concrete BootArchive implementation * @author Andy Wilkinson * @author Scott Frederick + * @author Moritz Halbritter */ abstract class AbstractBootArchiveTests { @@ -496,7 +497,7 @@ abstract class AbstractBootArchiveTests { assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath); assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) .isEqualTo(this.indexPath + "layers.idx"); - assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.TOOLS.getName()); } } @@ -530,7 +531,7 @@ abstract class AbstractBootArchiveTests { List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"dependencies\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); @@ -584,7 +585,7 @@ abstract class AbstractBootArchiveTests { List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"my-deps\":"); expected.add(" - \"" + layerToolsJar + "\""); @@ -614,15 +615,32 @@ abstract class AbstractBootArchiveTests { @Test void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { List entryNames = getEntryNames(createLayeredJar()); - assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + assertThat(entryNames).contains(this.libPath + JarModeLibrary.TOOLS.getName()); } @Test + void shouldAddToolsToTheJar() throws IOException { + this.task.getMainClass().set("com.example.Main"); + executeTask(); + List entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile()); + assertThat(entryNames).isNotEmpty().contains(this.libPath + JarModeLibrary.TOOLS.getName()); + } + + @Test + @SuppressWarnings("removal") void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { List entryNames = getEntryNames( createLayeredJar((configuration) -> configuration.getIncludeLayerTools().set(false))); - assertThat(entryNames).isNotEmpty() - .doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); + assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName()); + } + + @Test + void whenIncludeToolsIsFalseThenToolsAreNotAddedToTheJar() throws IOException { + this.task.getIncludeTools().set(false); + this.task.getMainClass().set("com.example.Main"); + executeTask(); + List entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile()); + assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName()); } protected File jarFile(String name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index d83e54ed165..cf14bba28c3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { assertThat(output).containsPattern("1\\. .*classes"); assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar"); assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar"); - assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-layertools.*.jar"); + assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar"); assertThat(output).doesNotContain("5. "); } @@ -77,7 +77,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { BuildResult result = this.gradleBuild.build("launch"); String output = result.getOutput(); assertThat(output).containsPattern("1\\. .*classes"); - assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-layertools.*.jar"); + assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar"); assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar"); assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar"); assertThat(output).doesNotContain("5. "); 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 3ba806e07e0..3103d3a44fd 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 @@ -38,12 +38,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle index cdbb87315a6..2e42641b008 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle @@ -17,7 +17,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 941f20aa4c9..803b09d444e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index d035cf456ef..a412d8d01b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootJar { } bootJar { - layered { - enabled = false - } + includeTools = false } 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 cc3aa6f0e8b..131fb7d18ce 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 @@ -21,12 +21,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle index 970d90d116f..29e8f89004c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -19,7 +19,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } 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 1f18bb3ebc2..f79bdd4414a 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 @@ -24,12 +24,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } 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 f8127b2f3db..eae79719225 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 @@ -55,12 +55,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } 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 ba34cf4d1d6..16986fb0668 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 @@ -33,12 +33,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootJar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle new file mode 100644 index 00000000000..583ab4fa370 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + {includeTools} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 7f4ca313065..27347023a82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootJar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index 45041d1c190..d0dfd4e27f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootJar { } bootJar { - layered { - enabled = false - } + includeTools = false } 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 0c4bcdcaf06..a643858e224 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 @@ -39,12 +39,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 85aea3ecce2..b04983661b9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index 184c97603e2..3cbd4aa11f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootWar { } bootWar { - layered { - enabled = false - } + includeTools = false } 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 6fd9018c455..07cda46020a 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 @@ -22,12 +22,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle index 60e32af928b..a8c43a25016 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -19,7 +19,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } 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 6892f814bbb..20daaf0f3aa 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 @@ -25,12 +25,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } 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 da574d0d153..c798b6bf190 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 @@ -56,12 +56,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } 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 cba40d5c3d3..8f467a84b68 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 @@ -34,12 +34,12 @@ dependencies { task listLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "list" + systemProperties = [ "jarmode": "tools" ] + args "list-layers" } task extractLayers(type: JavaExec) { classpath = bootWar.outputs.files - systemProperties = [ "jarmode": "layertools" ] - args "extract" + systemProperties = [ "jarmode": "tools" ] + args "extract", "--layers", "--launcher" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle new file mode 100644 index 00000000000..851db4027a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + {includeTools} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index f2d285e4081..00efac247c1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -18,7 +18,5 @@ dependencies { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index de8e9d65217..5688972529c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -21,7 +21,5 @@ bootWar { } bootWar { - layered { - enabled = false - } + includeTools = false } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java deleted file mode 100644 index c9f661facb1..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.jarmode.layertools; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.attribute.BasicFileAttributeView; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import org.springframework.util.Assert; -import org.springframework.util.StreamUtils; - -/** - * The {@code 'extract'} tools command. - * - * @author Phillip Webb - */ -class ExtractCommand extends Command { - - static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to"); - - private final Context context; - - private final Layers layers; - - ExtractCommand(Context context) { - this(context, Layers.get(context)); - } - - ExtractCommand(Context context, Layers layers) { - super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION), - Parameters.of("[...]")); - this.context = context; - this.layers = layers; - } - - @Override - protected void run(Map options, List parameters) { - try { - File destination = options.containsKey(DESTINATION_OPTION) ? new File(options.get(DESTINATION_OPTION)) - : this.context.getWorkingDir(); - for (String layer : this.layers) { - if (parameters.isEmpty() || parameters.contains(layer)) { - mkDirs(new File(destination, layer)); - } - } - try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) { - ZipEntry entry = zip.getNextEntry(); - Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString() - + "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled"); - while (entry != null) { - if (!entry.isDirectory()) { - String layer = this.layers.getLayer(entry); - if (parameters.isEmpty() || parameters.contains(layer)) { - write(zip, entry, new File(destination, layer)); - } - } - entry = zip.getNextEntry(); - } - } - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException { - String canonicalOutputPath = destination.getCanonicalPath() + File.separator; - File file = new File(destination, entry.getName()); - String canonicalEntryPath = file.getCanonicalPath(); - Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), - () -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath - + "'. This is outside the output location of '" + canonicalOutputPath - + "'. Verify the contents of your archive."); - mkParentDirs(file); - try (OutputStream out = new FileOutputStream(file)) { - StreamUtils.copy(zip, out); - } - try { - Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) - .setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); - } - catch (IOException ex) { - // File system does not support setting time attributes. Continue. - } - } - - private void mkParentDirs(File file) throws IOException { - mkDirs(file.getParentFile()); - } - - private void mkDirs(File file) throws IOException { - if (!file.exists() && !file.mkdirs()) { - throw new IOException("Unable to create directory " + file); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java deleted file mode 100644 index 7ac5c2d8ae9..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 org.springframework.boot.jarmode.layertools; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.List; - -import org.springframework.boot.loader.jarmode.JarMode; - -/** - * {@link JarMode} providing {@code "layertools"} support. - * - * @author Phillip Webb - * @author Scott Frederick - * @since 2.3.0 - */ -public class LayerToolsJarMode implements JarMode { - - @Override - public boolean accepts(String mode) { - return "layertools".equalsIgnoreCase(mode); - } - - @Override - public void run(String mode, String[] args) { - try { - new Runner().run(args); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - static class Runner { - - static Context contextOverride; - - private final List commands; - - private final HelpCommand help; - - Runner() { - Context context = (contextOverride != null) ? contextOverride : new Context(); - this.commands = getCommands(context); - this.help = new HelpCommand(context, this.commands); - } - - private void run(String[] args) { - run(dequeOf(args)); - } - - private void run(Deque args) { - if (!args.isEmpty()) { - String commandName = args.removeFirst(); - Command command = Command.find(this.commands, commandName); - if (command != null) { - runCommand(command, args); - return; - } - printError("Unknown command \"" + commandName + "\""); - } - this.help.run(args); - } - - private void runCommand(Command command, Deque args) { - try { - command.run(args); - } - catch (UnknownOptionException ex) { - printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command"); - this.help.run(dequeOf(command.getName())); - } - catch (MissingValueException ex) { - printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() - + " command requires a value"); - this.help.run(dequeOf(command.getName())); - } - } - - private void printError(String errorMessage) { - System.out.println("Error: " + errorMessage); - System.out.println(); - } - - private Deque dequeOf(String... args) { - return new ArrayDeque<>(Arrays.asList(args)); - } - - static List getCommands(Context context) { - List commands = new ArrayList<>(); - commands.add(new ListCommand(context)); - commands.add(new ExtractCommand(context)); - return Collections.unmodifiableList(commands); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 850c9c453d3..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -# Jar Modes -org.springframework.boot.loader.jarmode.JarMode=\ -org.springframework.boot.jarmode.layertools.LayerToolsJarMode \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java deleted file mode 100644 index 9acb9c9c6ca..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.jarmode.layertools; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.jar.JarEntry; -import java.util.zip.ZipOutputStream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.core.io.ClassPathResource; -import org.springframework.util.FileCopyUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HelpCommand}. - * - * @author Phillip Webb - */ -class HelpCommandTests { - - private HelpCommand command; - - private TestPrintStream out; - - @TempDir - File temp; - - @BeforeEach - void setup() throws Exception { - Context context = mock(Context.class); - given(context.getArchiveFile()).willReturn(createJarFile("test.jar")); - this.command = new HelpCommand(context, LayerToolsJarMode.Runner.getCommands(context)); - this.out = new TestPrintStream(this); - } - - @Test - void runWhenHasNoParametersPrintsUsage() { - this.command.run(this.out, Collections.emptyList()); - assertThat(this.out).hasSameContentAsResource("help-output.txt"); - } - - @Test - void runWhenHasNoCommandParameterPrintsUsage() { - this.command.run(this.out, Arrays.asList("extract")); - System.out.println(this.out); - assertThat(this.out).hasSameContentAsResource("help-extract-output.txt"); - } - - private File createJarFile(String name) throws Exception { - File file = new File(this.temp, name); - try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) { - jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); - jarOutputStream.write(getFile("test-manifest.MF").getBytes()); - jarOutputStream.closeEntry(); - JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); - jarOutputStream.putNextEntry(indexEntry); - Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); - writer.write("- \"0001\":\n"); - writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); - writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); - writer.write("- \"0002\":\n"); - writer.write(" - \"BOOT-INF/lib/c.jar\"\n"); - writer.write("- \"0003\":\n"); - writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); - writer.flush(); - } - return file; - } - - private String getFile(String fileName) throws Exception { - ClassPathResource resource = new ClassPathResource(fileName, getClass()); - InputStreamReader reader = new InputStreamReader(resource.getInputStream()); - return FileCopyUtils.copyToString(reader); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt deleted file mode 100644 index 0d2f9e143cf..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt +++ /dev/null @@ -1,7 +0,0 @@ -Extracts layers from the jar for image creation - -Usage: - java -Djarmode=layertools -jar test.jar extract [options] [...] - -Options: - --destination string The destination to extract files to 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-tools/build.gradle similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle index 96d50392499..7a0a9e911b2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle @@ -4,7 +4,7 @@ plugins { id "org.springframework.boot.deployed" } -description = "Spring Boot Layers Tools" +description = "Spring Boot Jarmode Tools" dependencies { implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java similarity index 76% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java index c6669b0d541..b0aee1d8ee8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,9 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; +import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,13 +25,15 @@ import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.stream.Stream; /** - * A command that can be launched from the layertools jarmode. + * A command that can be launched. * * @author Phillip Webb * @author Scott Frederick + * @author Moritz Halbritter */ abstract class Command { @@ -90,9 +93,10 @@ abstract class Command { /** * Run the command by processing the remaining arguments. + * @param out stream for command output * @param args a mutable deque of the remaining arguments */ - final void run(Deque args) { + final void run(PrintStream out, Deque args) { List parameters = new ArrayList<>(); Map options = new HashMap<>(); while (!args.isEmpty()) { @@ -105,15 +109,32 @@ abstract class Command { parameters.add(arg); } } - run(options, parameters); + run(out, options, parameters); } /** * Run the actual command. + * @param out stream for command output * @param options any options extracted from the arguments * @param parameters any parameters extracted from the arguments */ - protected abstract void run(Map options, List parameters); + abstract void run(PrintStream out, Map options, List parameters); + + /** + * Whether the command is deprecated. + * @return whether the command is deprecated + */ + boolean isDeprecated() { + return false; + } + + /** + * Returns the deprecation message. + * @return the deprecation message + */ + String getDeprecationMessage() { + return null; + } /** * Static method that can be used to find a single command from a collection. @@ -133,7 +154,7 @@ abstract class Command { /** * Parameters that the command accepts. */ - protected static final class Parameters { + static final class Parameters { private final List descriptions; @@ -158,7 +179,7 @@ abstract class Command { * Factory method used if there are no expected parameters. * @return a new {@link Parameters} instance */ - protected static Parameters none() { + static Parameters none() { return of(); } @@ -168,7 +189,7 @@ abstract class Command { * @param descriptions the parameter descriptions * @return a new {@link Parameters} instance with the given descriptions */ - protected static Parameters of(String... descriptions) { + static Parameters of(String... descriptions) { return new Parameters(descriptions); } @@ -177,7 +198,7 @@ abstract class Command { /** * Options that the command accepts. */ - protected static final class Options { + static final class Options { private final Option[] values; @@ -218,7 +239,7 @@ abstract class Command { * Factory method used if there are no expected options. * @return a new {@link Options} instance */ - protected static Options none() { + static Options none() { return of(); } @@ -228,7 +249,7 @@ abstract class Command { * @param values the option values * @return a new {@link Options} instance with the given values */ - protected static Options of(Option... values) { + static Options of(Option... values) { return new Options(values); } @@ -237,9 +258,9 @@ abstract class Command { /** * An individual option that the command can accepts. Can either be an option with a * value (e.g. {@literal --log debug}) or a flag (e.g. {@literal - * --verbose}). + * --verbose}). It also can be both if the value is marked as optional. */ - protected static final class Option { + static final class Option { private final String name; @@ -247,10 +268,13 @@ abstract class Command { private final String description; - private Option(String name, String valueDescription, String description) { + private final boolean optionalValue; + + private Option(String name, String valueDescription, String description, boolean optionalValue) { this.name = name; this.description = description; this.valueDescription = valueDescription; + this.optionalValue = optionalValue; } /** @@ -287,13 +311,24 @@ abstract class Command { } private String claimArg(Deque args) { - if (this.valueDescription != null) { - if (args.isEmpty()) { - throw new MissingValueException(this.name); + if (this.valueDescription == null) { + return null; + } + if (this.optionalValue) { + String nextArg = args.peek(); + if (nextArg == null || nextArg.startsWith("--")) { + return null; } return args.removeFirst(); } - return null; + else { + try { + return args.removeFirst(); + } + catch (NoSuchElementException ex) { + throw new MissingValueException(this.name); + } + } } @Override @@ -323,8 +358,19 @@ abstract class Command { * @param description a description of the option * @return a new {@link Option} instance */ - protected static Option flag(String name, String description) { - return new Option(name, null, description); + static Option flag(String name, String description) { + return new Option(name, null, description, false); + } + + /** + * Factory method to create value option. + * @param name the name of the option + * @param valueDescription a description of the expected value + * @param description a description of the option + * @return a new {@link Option} instance + */ + static Option of(String name, String valueDescription, String description) { + return new Option(name, valueDescription, description, false); } /** @@ -332,10 +378,11 @@ abstract class Command { * @param name the name of the option * @param valueDescription a description of the expected value * @param description a description of the option + * @param optionalValue whether the value is optional * @return a new {@link Option} instance */ - protected static Option of(String name, String valueDescription, String description) { - return new Option(name, valueDescription, description); + static Option of(String name, String valueDescription, String description, boolean optionalValue) { + return new Option(name, valueDescription, description, optionalValue); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java index 4be9b946f4c..32f7976cddf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.IOException; 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 new file mode 100644 index 00000000000..dace9d19437 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -0,0 +1,436 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.springframework.boot.jarmode.tools.JarStructure.Entry; +import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; +import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * The {@code 'extract'} tools command. + * + * @author Moritz Halbritter + */ +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"); + + /** + * Option to extract layers. + */ + static final Option LAYERS_OPTION = Option.of("layers", "string list", "Layers to extract", true); + + /** + * 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"); + + 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 final Context context; + + private final Layers layers; + + ExtractCommand(Context context) { + this(context, null); + } + + 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()); + this.context = context; + this.layers = layers; + } + + @Override + void run(PrintStream out, Map options, List parameters) { + try { + checkJarCompatibility(); + File destination = getWorkingDirectory(options); + FileResolver fileResolver = getFileResolver(destination, options); + fileResolver.createDirectories(); + if (options.containsKey(LAUNCHER_OPTION)) { + extractArchive(fileResolver); + } + else { + JarStructure jarStructure = getJarStructure(); + extractLibraries(fileResolver, jarStructure, options); + createRunner(jarStructure, fileResolver, options); + } + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + catch (LayersNotEnabledException ex) { + printError(out, "Layers are not enabled"); + } + } + + 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)); + } + } + + private void printError(PrintStream out, String message) { + out.println("Error: " + message); + out.println(); + } + + private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map options) + throws IOException { + String librariesDirectory = getLibrariesDirectory(options); + extractArchive(fileResolver, (zipEntry) -> { + Entry entry = jarStructure.resolve(zipEntry); + if (isType(entry, Type.LIBRARY)) { + return librariesDirectory + entry.location(); + } + return null; + }); + } + + private static String getLibrariesDirectory(Map options) { + if (options.containsKey(LIBRARIES_DIRECTORY_OPTION)) { + String value = options.get(LIBRARIES_DIRECTORY_OPTION); + if (value.endsWith("/")) { + return value; + } + return value + "/"; + } + return "lib/"; + } + + private FileResolver getFileResolver(File destination, Map options) { + String runnerFilename = getRunnerFilename(options); + if (!options.containsKey(LAYERS_OPTION)) { + return new NoLayersFileResolver(destination, runnerFilename); + } + Layers layers = getLayers(); + Set layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); + return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename); + } + + private File getWorkingDirectory(Map options) { + if (options.containsKey(DESTINATION_OPTION)) { + return new File(options.get(DESTINATION_OPTION)); + } + return this.context.getWorkingDir(); + } + + private JarStructure getJarStructure() { + IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context.getArchiveFile()); + Assert.state(jarStructure != null, "Couldn't read classpath index"); + return jarStructure; + } + + private void extractArchive(FileResolver fileResolver) throws IOException { + extractArchive(fileResolver, ZipEntry::getName); + } + + private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer) + throws IOException { + withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> { + if (zipEntry.isDirectory()) { + return; + } + String name = entryNameTransformer.getName(zipEntry); + if (name == null) { + return; + } + File file = fileResolver.resolve(zipEntry, name); + if (file != null) { + extractEntry(stream, zipEntry, file); + } + }); + } + + private Layers getLayers() { + if (this.layers != null) { + return this.layers; + } + return Layers.get(this.context); + } + + private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map options) + throws IOException { + File file = fileResolver.resolveRunner(); + if (file == null) { + return; + } + String librariesDirectory = getLibrariesDirectory(options); + Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library); + mkDirs(file.getParentFile()); + try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) { + withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> { + Entry entry = jarStructure.resolve(zipEntry); + if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) { + JarEntry jarEntry = createJarEntry(entry.location(), zipEntry); + output.putNextEntry(jarEntry); + StreamUtils.copy(stream, output); + output.closeEntry(); + } + })); + } + } + + private String getRunnerFilename(Map options) { + if (options.containsKey(RUNNER_FILENAME_OPTION)) { + return options.get(RUNNER_FILENAME_OPTION); + } + return "runner.jar"; + } + + private static boolean isType(Entry entry, Type type) { + if (entry == null) { + return false; + } + return entry.type() == type; + } + + private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException { + mkDirs(file.getParentFile()); + try (OutputStream out = new FileOutputStream(file)) { + StreamUtils.copy(zip, out); + } + try { + Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) + .setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); + } + catch (IOException ex) { + // File system does not support setting time attributes. Continue. + } + } + + private static void mkDirs(File file) throws IOException { + if (!file.exists() && !file.mkdirs()) { + throw new IOException("Unable to create directory " + file); + } + } + + private static JarEntry createJarEntry(String location, ZipEntry originalEntry) { + JarEntry jarEntry = new JarEntry(location); + FileTime lastModifiedTime = originalEntry.getLastModifiedTime(); + if (lastModifiedTime != null) { + jarEntry.setLastModifiedTime(lastModifiedTime); + } + FileTime lastAccessTime = originalEntry.getLastAccessTime(); + if (lastAccessTime != null) { + jarEntry.setLastAccessTime(lastAccessTime); + } + FileTime creationTime = originalEntry.getCreationTime(); + if (creationTime != null) { + jarEntry.setCreationTime(creationTime); + } + return jarEntry; + } + + private static void withZipEntries(File file, ThrowingConsumer callback) throws IOException { + try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) { + ZipEntry entry = stream.getNextEntry(); + while (entry != null) { + if (StringUtils.hasLength(entry.getName())) { + callback.accept(stream, entry); + } + entry = stream.getNextEntry(); + } + } + } + + private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException { + String canonicalOutputPath = directory.getCanonicalPath() + File.separator; + String canonicalEntryPath = file.getCanonicalPath(); + Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), + () -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." + .formatted(name, canonicalEntryPath, canonicalOutputPath)); + return file; + } + + @FunctionalInterface + private interface EntryNameTransformer { + + String getName(ZipEntry entry); + + } + + @FunctionalInterface + private interface ThrowingConsumer { + + void accept(ZipInputStream stream, ZipEntry entry) throws IOException; + + } + + private interface FileResolver { + + /** + * Creates needed directories. + * @throws IOException if something went wrong + */ + void createDirectories() throws IOException; + + /** + * Resolves the given {@link ZipEntry} to a file. + * @param entry the zip entry + * @param newName the new name of the file + * @return file where the contents should be written or {@code null} if this entry + * should be skipped + * @throws IOException if something went wrong + */ + default File resolve(ZipEntry entry, String newName) throws IOException { + return resolve(entry.getName(), newName); + } + + /** + * Resolves the given name to a file. + * @param originalName the original name of the file + * @param newName the new name of the file + * @return file where the contents should be written or {@code null} if this name + * should be skipped + * @throws IOException if something went wrong + */ + 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 + * @throws IOException if something went wrong + */ + File resolveRunner() throws IOException; + + } + + private static final class NoLayersFileResolver implements FileResolver { + + private final File directory; + + private final String runnerFilename; + + private NoLayersFileResolver(File directory, String runnerFilename) { + this.directory = directory; + this.runnerFilename = runnerFilename; + } + + @Override + public void createDirectories() { + } + + @Override + public File resolve(String originalName, String newName) throws IOException { + return assertFileIsContainedInDirectory(this.directory, new File(this.directory, newName), newName); + } + + @Override + public File resolveRunner() throws IOException { + return resolve(this.runnerFilename, this.runnerFilename); + } + + } + + private static final class LayersFileResolver implements FileResolver { + + private final Layers layers; + + private final Set layersToExtract; + + private final File directory; + + private final String runnerFilename; + + LayersFileResolver(File directory, Layers layers, Set layersToExtract, String runnerFilename) { + this.layers = layers; + this.layersToExtract = layersToExtract; + this.directory = directory; + this.runnerFilename = runnerFilename; + } + + @Override + public void createDirectories() throws IOException { + for (String layer : this.layers) { + if (shouldExtractLayer(layer)) { + mkDirs(getLayerDirectory(layer)); + } + } + } + + @Override + public File resolve(String originalName, String newName) throws IOException { + String layer = this.layers.getLayer(originalName); + if (shouldExtractLayer(layer)) { + File directory = getLayerDirectory(layer); + return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName); + } + return null; + } + + @Override + public File resolveRunner() 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 null; + } + + private File getLayerDirectory(String layer) { + return new File(this.directory, layer); + } + + private boolean shouldExtractLayer(String layer) { + if (this.layersToExtract.isEmpty()) { + return true; + } + return this.layersToExtract.contains(layer); + } + + } + +} 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 new file mode 100644 index 00000000000..b2770b9fe41 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.PrintStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * The {@code 'extract'} tools command. + * + * @author Phillip Webb + */ +class ExtractLayersCommand extends Command { + + static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to"); + + private final ExtractCommand delegate; + + ExtractLayersCommand(Context context) { + this(context, null); + } + + ExtractLayersCommand(Context context, Layers layers) { + super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION), + Parameters.of("[...]")); + this.delegate = new ExtractCommand(context, layers); + } + + @Override + boolean isDeprecated() { + return true; + } + + @Override + String getDeprecationMessage() { + return "Use '-Djarmode=tools extract --layers --launcher' instead."; + } + + @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.LAYERS_OPTION, StringUtils.collectionToCommaDelimitedString(parameters)); + rewrittenOptions.put(ExtractCommand.LAUNCHER_OPTION, null); + this.delegate.run(out, rewrittenOptions, Collections.emptyList()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java similarity index 58% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java index 4f25c80dc25..17188c941ce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.PrintStream; import java.util.List; @@ -25,6 +25,7 @@ import java.util.stream.Stream; * Implicit {@code 'help'} command. * * @author Phillip Webb + * @author Moritz Halbritter */ class HelpCommand extends Command { @@ -32,27 +33,47 @@ class HelpCommand extends Command { private final List commands; + private final String jarMode; + HelpCommand(Context context, List commands) { - super("help", "Help about any command", Options.none(), Parameters.of("[ commands, String jarMode) { + super("help", "Help about any command", Options.none(), Parameters.of("[]")); this.context = context; this.commands = commands; + this.jarMode = (jarMode != null) ? jarMode : "tools"; } @Override - protected void run(Map options, List parameters) { - run(System.out, parameters); + void run(PrintStream out, Map options, List parameters) { + run(out, parameters); } void run(PrintStream out, List parameters) { - Command command = (!parameters.isEmpty()) ? Command.find(this.commands, parameters.get(0)) : null; - if (command != null) { - printCommandHelp(out, command); + String commandName = (parameters.isEmpty()) ? null : parameters.get(0); + if (commandName == null) { + printUsageAndCommands(out); + return; + } + if (getName().equals(commandName)) { + printCommandHelp(out, this, true); + return; + } + Command command = Command.find(this.commands, commandName); + if (command == null) { + printError(out, "Unknown command \"%s\"".formatted(commandName)); + printUsageAndCommands(out); return; } - printUsageAndCommands(out); + printCommandHelp(out, command, true); } - private void printCommandHelp(PrintStream out, Command command) { + void printCommandHelp(PrintStream out, Command command, boolean printDeprecationWarning) { + if (command.isDeprecated() && printDeprecationWarning) { + printWarning(out, "This command is deprecated. " + command.getDeprecationMessage()); + } out.println(command.getDescription()); out.println(); out.println("Usage:"); @@ -85,8 +106,17 @@ class HelpCommand extends Command { out.println(); out.println("Available commands:"); int maxNameLength = getMaxLength(getName().length(), this.commands.stream().map(Command::getName)); - this.commands.forEach((command) -> printCommandSummary(out, command, maxNameLength)); + this.commands.stream() + .filter((command) -> !command.isDeprecated()) + .forEach((command) -> printCommandSummary(out, command, maxNameLength)); printCommandSummary(out, this, maxNameLength); + List deprecatedCommands = this.commands.stream().filter(Command::isDeprecated).toList(); + if (!deprecatedCommands.isEmpty()) { + out.println("Deprecated commands:"); + for (Command command : deprecatedCommands) { + printCommandSummary(out, command, maxNameLength); + } + } } private int getMaxLength(int minimum, Stream strings) { @@ -98,7 +128,17 @@ class HelpCommand extends Command { } private String getJavaCommand() { - return "java -Djarmode=layertools -jar " + this.context.getArchiveFile().getName(); + return "java -Djarmode=" + this.jarMode + " -jar " + this.context.getArchiveFile().getName(); + } + + private void printError(PrintStream out, String errorMessage) { + out.println("Error: " + errorMessage); + out.println(); + } + + private void printWarning(PrintStream out, String errorMessage) { + out.println("Warning: " + errorMessage); + out.println(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java new file mode 100644 index 00000000000..8c026bbf409 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * {@link JarStructure} implementation backed by a {@code classpath.idx} file. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + */ +class IndexedJarStructure implements JarStructure { + + private static final List MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes", + "Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index"); + + private final Manifest originalManifest; + + private final String libLocation; + + private final String classesLocation; + + private final List classpathEntries; + + IndexedJarStructure(Manifest originalManifest, String indexFile) { + this.originalManifest = originalManifest; + this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib"); + this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes"); + this.classpathEntries = readIndexFile(indexFile); + } + + private static String getLocation(Manifest manifest, String attribute) { + String location = getMandatoryAttribute(manifest, attribute); + if (!location.endsWith("/")) { + location = location + "/"; + } + return location; + } + + private static List readIndexFile(String indexFile) { + String[] lines = Arrays.stream(indexFile.split("\n")) + .map((line) -> line.replace("\r", "")) + .filter(StringUtils::hasText) + .toArray(String[]::new); + List classpathEntries = new ArrayList<>(); + for (String line : lines) { + if (line.startsWith("- ")) { + classpathEntries.add(line.substring(3, line.length() - 1)); + } + else { + throw new IllegalStateException("Classpath index file is malformed"); + } + } + Assert.state(!classpathEntries.isEmpty(), "Empty classpath index file loaded"); + return classpathEntries; + } + + @Override + public String getClassesLocation() { + return this.classesLocation; + } + + @Override + public Entry resolve(String name) { + if (this.classpathEntries.contains(name)) { + return new Entry(name, toStructureDependency(name), Type.LIBRARY); + } + else if (name.startsWith(this.classesLocation)) { + return new Entry(name, name.substring(this.classesLocation.length()), Type.APPLICATION_CLASS_OR_RESOURCE); + } + else if (name.startsWith("org/springframework/boot/loader")) { + return new Entry(name, name, Type.LOADER); + } + return null; + } + + @Override + public Manifest createLauncherManifest(UnaryOperator libraryTransformer) { + Manifest manifest = new Manifest(this.originalManifest); + Attributes attributes = manifest.getMainAttributes(); + for (String denied : MANIFEST_DENY_LIST) { + attributes.remove(new Name(denied)); + } + attributes.put(Name.MAIN_CLASS, getMandatoryAttribute(this.originalManifest, "Start-Class")); + attributes.put(Name.CLASS_PATH, + this.classpathEntries.stream() + .map(this::toStructureDependency) + .map(libraryTransformer) + .collect(Collectors.joining(" "))); + return manifest; + } + + private String toStructureDependency(String libEntryName) { + Assert.state(libEntryName.startsWith(this.libLocation), "Invalid library location " + libEntryName); + return libEntryName.substring(this.libLocation.length()); + } + + private static String getMandatoryAttribute(Manifest manifest, String attribute) { + String value = manifest.getMainAttributes().getValue(attribute); + Assert.state(value != null, "Manifest attribute '" + attribute + "' is mandatory"); + return value; + } + + static IndexedJarStructure get(File file) { + try { + try (JarFile jarFile = new JarFile(file)) { + Manifest manifest = jarFile.getManifest(); + String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index"); + ZipEntry entry = jarFile.getEntry(location); + if (entry != null) { + String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); + return new IndexedJarStructure(manifest, indexFile); + } + } + return null; + } + catch (FileNotFoundException | NoSuchFileException ex) { + return null; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java similarity index 83% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java index 33e2ae66e0a..0469aeed836 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.FileNotFoundException; import java.io.IOException; @@ -39,12 +39,16 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Madhura Bhave + * @author Moritz Halbritter */ class IndexedLayers implements Layers { private final Map> layers = new LinkedHashMap<>(); - IndexedLayers(String indexFile) { + private final String classesLocation; + + IndexedLayers(String indexFile, String classesLocation) { + this.classesLocation = classesLocation; String[] lines = Arrays.stream(indexFile.split("\n")) .map((line) -> line.replace("\r", "")) .filter(StringUtils::hasText) @@ -56,6 +60,7 @@ class IndexedLayers implements Layers { this.layers.put(line.substring(3, line.length() - 2), contents); } else if (line.startsWith(" - ")) { + Assert.notNull(contents, "Contents must not be null. Check if the index file is malformed!"); contents.add(line.substring(5, line.length() - 1)); } else { @@ -66,16 +71,17 @@ class IndexedLayers implements Layers { } @Override - public Iterator iterator() { - return this.layers.keySet().iterator(); + public String getApplicationLayerName() { + return getLayer(this.classesLocation); } @Override - public String getLayer(ZipEntry entry) { - return getLayer(entry.getName()); + public Iterator iterator() { + return this.layers.keySet().iterator(); } - private String getLayer(String name) { + @Override + public String getLayer(String name) { for (Map.Entry> entry : this.layers.entrySet()) { for (String candidate : entry.getValue()) { if (candidate.equals(name) || (candidate.endsWith("/") && name.startsWith(candidate))) { @@ -100,7 +106,8 @@ class IndexedLayers implements Layers { ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null; if (entry != null) { String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); - return new IndexedLayers(indexFile); + String classesLocation = manifest.getMainAttributes().getValue("Spring-Boot-Classes"); + return new IndexedLayers(indexFile, classesLocation); } } return null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java new file mode 100644 index 00000000000..372437eacd6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.util.function.UnaryOperator; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +/** + * Provide information about a fat jar structure that is meant to be extracted. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + */ +interface JarStructure { + + /** + * Resolve the specified {@link ZipEntry}, return {@code null} if the entry should not + * be handled. + * @param entry the entry to handle + * @return the resolved {@link Entry} + */ + default Entry resolve(ZipEntry entry) { + return resolve(entry.getName()); + } + + /** + * Resolve the entry with the specified name, return {@code null} if the entry should + * not be handled. + * @param name the name of the entry to handle + * @return the resolved {@link Entry} + */ + Entry resolve(String name); + + /** + * Create the {@link Manifest} for the launcher jar, applying the specified operator + * on each classpath entry. + * @param libraryTransformer the operator to apply on each classpath entry + * @return the manifest to use for the launcher jar + */ + Manifest createLauncherManifest(UnaryOperator libraryTransformer); + + /** + * Return the location of the application classes. + * @return the location of the application classes + */ + String getClassesLocation(); + + /** + * An entry to handle in the exploded structure. + * + * @param originalLocation the original location + * @param location the relative location + * @param type of the entry + */ + record Entry(String originalLocation, String location, Type type) { + enum Type { + + LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER + + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java new file mode 100644 index 00000000000..851afd18019 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.util.List; + +import org.springframework.boot.loader.jarmode.JarMode; + +/** + * {@link JarMode} providing {@code "layertools"} support. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class LayerToolsJarMode implements JarMode { + + static Context contextOverride; + + @Override + public boolean accepts(String mode) { + return "layertools".equalsIgnoreCase(mode); + } + + @Override + public void run(String mode, String[] args) { + try { + Context context = (contextOverride != null) ? contextOverride : new Context(); + new Runner(System.out, context, getCommands(context)).run(args); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + static List getCommands(Context context) { + return List.of(new ListCommand(context), new ExtractLayersCommand(context)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java similarity index 63% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java index 181efb3cd84..05eb07ebdc9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.util.Iterator; import java.util.zip.ZipEntry; @@ -23,6 +23,7 @@ import java.util.zip.ZipEntry; * Provides information about the jar layers. * * @author Phillip Webb + * @author Moritz Halbritter * @see ExtractCommand * @see ListCommand */ @@ -40,19 +41,43 @@ interface Layers extends Iterable { * @param entry the entry to check * @return the layer that the entry is in */ - String getLayer(ZipEntry entry); + default String getLayer(ZipEntry entry) { + return getLayer(entry.getName()); + } + + /** + * Return the layer that the entry with the given name is in. + * @param entryName the name of the entry to check + * @return the layer that the entry is in + */ + String getLayer(String entryName); + + /** + * Return the name of the application layer. + * @return the name of the application layer + */ + String getApplicationLayerName(); /** * Return a {@link Layers} instance for the currently running application. * @param context the command context * @return a new layers instance + * @throws LayersNotEnabledException if layers are not enabled */ static Layers get(Context context) { IndexedLayers indexedLayers = IndexedLayers.get(context); if (indexedLayers == null) { - throw new IllegalStateException("Failed to load layers.idx which is required by layertools"); + throw new LayersNotEnabledException(); } return indexedLayers; } + final class LayersNotEnabledException extends RuntimeException { + + LayersNotEnabledException() { + super("Layers not enabled: Failed to load layer index file"); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java similarity index 60% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java index b08a7924944..fd5af54828b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.PrintStream; import java.util.List; @@ -23,24 +23,37 @@ import java.util.Map; /** * The {@code 'list'} tools command. * + * Delegates the actual work to {@link ListLayersCommand}. + * * @author Phillip Webb + * @author Moritz Halbritter */ class ListCommand extends Command { - private final Context context; + private final ListLayersCommand delegate; ListCommand(Context context) { super("list", "List layers from the jar that can be extracted", Options.none(), Parameters.none()); - this.context = context; + this.delegate = new ListLayersCommand(context); + } + + @Override + boolean isDeprecated() { + return true; + } + + @Override + String getDeprecationMessage() { + return "Use '-Djarmode=tools list-layers' instead."; } @Override - protected void run(Map options, List parameters) { - printLayers(Layers.get(this.context), System.out); + void run(PrintStream out, Map options, List parameters) { + this.delegate.run(out, options, parameters); } void printLayers(Layers layers, PrintStream out) { - layers.forEach(out::println); + this.delegate.printLayers(out, layers); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java new file mode 100644 index 00000000000..a2b122ec21e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.PrintStream; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; + +/** + * The {@code 'list-layers'} tools command. + * + * @author Moritz Halbritter + */ +class ListLayersCommand extends Command { + + private final Context context; + + ListLayersCommand(Context context) { + super("list-layers", "List layers from the jar that can be extracted", Options.none(), Parameters.none()); + this.context = context; + } + + @Override + void run(PrintStream out, Map options, List parameters) { + try { + Layers layers = Layers.get(this.context); + printLayers(out, layers); + } + catch (LayersNotEnabledException ex) { + printError(out, "Layers are not enabled"); + } + } + + void printLayers(PrintStream out, Layers layers) { + layers.forEach(out::println); + } + + private void printError(PrintStream out, String message) { + out.println("Error: " + message); + out.println(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java index f5fa8570e94..c4c08474899 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; /** * Exception thrown when a required value is not provided for an option. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java new file mode 100644 index 00000000000..71c0617cb75 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +/** + * Runs commands. + * + * @author Moritz Halbritter + */ +class Runner { + + private final PrintStream out; + + private final List commands = new ArrayList<>(); + + private final HelpCommand help; + + Runner(PrintStream out, Context context, List commands) { + this.out = out; + this.commands.addAll(commands); + this.help = new HelpCommand(context, commands); + this.commands.add(this.help); + } + + void run(String... args) { + run(dequeOf(args)); + } + + private void run(Deque args) { + if (!args.isEmpty()) { + String commandName = args.removeFirst(); + Command command = Command.find(this.commands, commandName); + if (command != null) { + runCommand(command, args); + return; + } + printError("Unknown command \"" + commandName + "\""); + } + this.help.run(this.out, args); + } + + private void runCommand(Command command, Deque args) { + if (command.isDeprecated()) { + printWarning("This command is deprecated. " + command.getDeprecationMessage()); + } + try { + command.run(this.out, args); + } + catch (UnknownOptionException ex) { + printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command"); + this.help.printCommandHelp(this.out, command, false); + } + catch (MissingValueException ex) { + printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() + " command requires a value"); + this.help.printCommandHelp(this.out, command, false); + } + } + + private void printWarning(String message) { + this.out.println("Warning: " + message); + this.out.println(); + } + + private void printError(String message) { + this.out.println("Error: " + message); + this.out.println(); + } + + private Deque dequeOf(String... args) { + return new ArrayDeque<>(Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java new file mode 100644 index 00000000000..3e70734d3f2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.PrintStream; +import java.util.List; + +import org.springframework.boot.loader.jarmode.JarMode; + +/** + * {@link JarMode} providing {@code "tools"} support. + * + * @author Moritz Halbritter + * @since 3.3.0 + */ +public class ToolsJarMode implements JarMode { + + private final Context context; + + private final PrintStream out; + + public ToolsJarMode() { + this(null, null); + } + + public ToolsJarMode(Context context, PrintStream out) { + this.context = (context != null) ? context : new Context(); + this.out = (out != null) ? out : System.out; + } + + @Override + public boolean accepts(String mode) { + return "tools".equalsIgnoreCase(mode); + } + + @Override + public void run(String mode, String[] args) { + try { + new Runner(this.out, this.context, getCommands(this.context)).run(args); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + static List getCommands(Context context) { + return List.of(new ExtractCommand(context), new ListLayersCommand(context)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java index 65d55413071..ba368ddef5a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; /** * Exception thrown when an unrecognized option is encountered. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java index dff2cc43578..8686163fe26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java @@ -15,6 +15,6 @@ */ /** - * JarMode support for layertools. + * JarMode support for layertools and tools. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..bc571075c5f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Jar Modes +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.jarmode.tools.LayerToolsJarMode,\ +org.springframework.boot.jarmode.tools.ToolsJarMode diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java new file mode 100644 index 00000000000..1ffa0bfe603 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarFile; +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.io.TempDir; + +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Moritz Halbritter + */ +abstract class AbstractTests { + + @TempDir + File tempDir; + + Manifest createManifest(String... entries) { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + for (String entry : entries) { + int colon = entry.indexOf(':'); + Assert.state(colon > -1, () -> "Colon not found in %s".formatted(entry)); + String key = entry.substring(0, colon).trim(); + String value = entry.substring(colon + 1).trim(); + manifest.getMainAttributes().putValue(key, value); + } + return manifest; + } + + File createArchive(String... entries) throws IOException { + return createArchive(createManifest(), entries); + } + + File createArchive(Manifest manifest, String... entries) throws IOException { + return createArchive(manifest, null, null, null, entries); + } + + File createArchive(Manifest manifest, Instant creationTime, Instant lastModifiedTime, Instant lastAccessTime, + String... entries) throws IOException { + Assert.state(entries.length % 2 == 0, "Entries must be key value pairs"); + File file = new File(this.tempDir, "test.jar"); + try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), manifest)) { + for (int i = 0; i < entries.length; i += 2) { + ZipEntry entry = new ZipEntry(entries[i]); + if (creationTime != null) { + entry.setCreationTime(FileTime.from(creationTime)); + } + if (lastModifiedTime != null) { + entry.setLastModifiedTime(FileTime.from(lastModifiedTime)); + } + if (lastAccessTime != null) { + entry.setLastAccessTime(FileTime.from(lastAccessTime)); + } + jar.putNextEntry(entry); + String resource = entries[i + 1]; + if (resource != null) { + try (InputStream content = ListLayersCommandTests.class.getResourceAsStream(resource)) { + assertThat(content).as("Resource " + resource).isNotNull(); + StreamUtils.copy(content, jar); + } + } + jar.closeEntry(); + } + } + return file; + } + + TestPrintStream runCommand(CommandFactory commandFactory, File archive, String... arguments) { + Context context = new Context(archive, this.tempDir); + Command command = commandFactory.create(context); + TestPrintStream out = new TestPrintStream(this); + command.run(out, new ArrayDeque<>(Arrays.asList(arguments))); + return out; + } + + Manifest getJarManifest(File jar) throws IOException { + try (JarFile jarFile = new JarFile(jar)) { + return jarFile.getManifest(); + } + } + + Map getJarManifestAttributes(File jar) throws IOException { + assertThat(jar).exists(); + Manifest manifest = getJarManifest(jar); + Map result = new HashMap<>(); + manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString())); + return result; + } + + List getJarEntryNames(File jar) throws IOException { + assertThat(jar).exists(); + try (JarFile jarFile = new JarFile(jar)) { + return jarFile.stream().map(ZipEntry::getName).toList(); + } + } + + List listFilenames() throws IOException { + return listFilenames(this.tempDir); + } + + List listFilenames(File directory) throws IOException { + try (Stream stream = Files.walk(directory.toPath())) { + int substring = directory.getAbsolutePath().length() + 1; + return stream.map((file) -> file.toAbsolutePath().toString()) + .map((file) -> (file.length() >= substring) ? file.substring(substring) : "") + .filter(StringUtils::hasLength) + .toList(); + } + } + + interface CommandFactory { + + T create(Context context); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java index 4293af4fa2a..c16653dc2f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,9 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; +import java.io.PrintStream; import java.util.ArrayDeque; import java.util.Arrays; import java.util.List; @@ -24,9 +25,9 @@ import java.util.Map; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import org.springframework.boot.jarmode.layertools.Command.Option; -import org.springframework.boot.jarmode.layertools.Command.Options; -import org.springframework.boot.jarmode.layertools.Command.Parameters; +import org.springframework.boot.jarmode.tools.Command.Option; +import org.springframework.boot.jarmode.tools.Command.Options; +import org.springframework.boot.jarmode.tools.Command.Parameters; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * * @author Phillip Webb * @author Scott Frederick + * @author Moritz Halbritter */ class CommandTests { @@ -44,6 +46,9 @@ class CommandTests { private static final Option LOG_LEVEL_OPTION = Option.of("log-level", "Logging level (debug or info)", "string"); + private static final Option LAYERS_OPTION = Option.of("layers", "Layers (leave empty for all)", "string list", + true); + @Test void getNameReturnsName() { TestCommand command = new TestCommand("test"); @@ -146,8 +151,16 @@ class CommandTests { assertThat(option.getValueDescription()).isEqualTo("value description"); } + @Test + void shouldNotParseFollowingOptionAsValue() { + TestCommand command = new TestCommand("test", LAYERS_OPTION, LOG_LEVEL_OPTION); + run(command, "--layers", "--log-level", "debug"); + assertThat(command.getRunOptions()).containsEntry(LAYERS_OPTION, null); + assertThat(command.getRunOptions()).containsEntry(LOG_LEVEL_OPTION, "debug"); + } + private void run(TestCommand command, String... args) { - command.run(new ArrayDeque<>(Arrays.asList(args))); + command.run(System.out, new ArrayDeque<>(Arrays.asList(args))); } static class TestCommand extends Command { @@ -165,7 +178,7 @@ class CommandTests { } @Override - protected void run(Map options, List parameters) { + protected void run(PrintStream out, Map options, List parameters) { this.runOptions = options; this.runParameters = parameters; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java index 44c6c5b0a0d..0d4c9c53e8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.IOException; 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 new file mode 100644 index 00000000000..e6a50f36333 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -0,0 +1,303 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.Runtime.Version; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.condition.OS; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ExtractCommand}. + * + * @author Moritz Halbritter + */ +class ExtractCommandTests extends AbstractTests { + + private static final Instant NOW = Instant.now(); + + private static final Instant CREATION_TIME = NOW.minus(3, ChronoUnit.DAYS); + + private static final Instant LAST_MODIFIED_TIME = NOW.minus(2, ChronoUnit.DAYS); + + private static final Instant LAST_ACCESS_TIME = NOW.minus(1, ChronoUnit.DAYS); + + private File archive; + + @BeforeEach + void setUp() throws IOException { + Manifest manifest = createManifest("Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx", + "Spring-Boot-Lib: BOOT-INF/lib/", "Spring-Boot-Classes: BOOT-INF/classes/", + "Start-Class: org.example.Main", "Spring-Boot-Layers-Index: BOOT-INF/layers.idx", + "Some-Attribute: Some-Value"); + this.archive = createArchive(manifest, CREATION_TIME, LAST_MODIFIED_TIME, LAST_ACCESS_TIME, + "BOOT-INF/classpath.idx", "/jar-contents/classpath.idx", "BOOT-INF/layers.idx", + "/jar-contents/layers.idx", "BOOT-INF/lib/dependency-1.jar", "/jar-contents/dependency-1", + "BOOT-INF/lib/dependency-2.jar", "/jar-contents/dependency-2", "BOOT-INF/lib/dependency-3-SNAPSHOT.jar", + "/jar-contents/dependency-3-SNAPSHOT", "org/springframework/boot/loader/launch/JarLauncher.class", + "/jar-contents/JarLauncher", "BOOT-INF/classes/application.properties", + "/jar-contents/application.properties"); + } + + private File file(String name) { + return new File(this.tempDir, name); + } + + private TestPrintStream run(File archive, String... args) { + return runCommand(ExtractCommand::new, archive, args); + } + + private void timeAttributes(File file) { + try { + BasicFileAttributes basicAttributes = Files + .getFileAttributeView(file.toPath(), BasicFileAttributeView.class) + .readAttributes(); + assertThat(basicAttributes.lastModifiedTime().toInstant().truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(LAST_MODIFIED_TIME.truncatedTo(ChronoUnit.SECONDS)); + Instant expectedCreationTime = expectedCreationTime(); + if (expectedCreationTime != null) { + assertThat(basicAttributes.creationTime().toInstant().truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(expectedCreationTime.truncatedTo(ChronoUnit.SECONDS)); + } + assertThat(basicAttributes.lastAccessTime().toInstant().truncatedTo(ChronoUnit.SECONDS)) + .isEqualTo(LAST_ACCESS_TIME.truncatedTo(ChronoUnit.SECONDS)); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private Instant expectedCreationTime() { + // macOS uses last modified time until Java 20 where it uses creation time. + // https://github.com/openjdk/jdk21u-dev/commit/6397d564a5dab07f81bf4c69b116ebfabb2446ba + if (OS.MAC.isCurrentOs()) { + return (EnumSet.range(JRE.JAVA_17, JRE.JAVA_19).contains(JRE.currentVersion())) ? LAST_MODIFIED_TIME + : CREATION_TIME; + } + if (OS.LINUX.isCurrentOs()) { + // Linux uses the modified time until Java 21.0.2 where a bug means that it + // uses the birth time which it has not set, preventing us from verifying it. + // https://github.com/openjdk/jdk21u-dev/commit/4cf572e3b99b675418e456e7815fb6fd79245e30 + return (Runtime.version().compareTo(Version.parse("21.0.2")) >= 0) ? null : LAST_MODIFIED_TIME; + } + return CREATION_TIME; + } + + @Nested + class Extract { + + @Test + void extractLibrariesAndCreatesRunner() 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"); + } + + @Test + void extractLibrariesAndCreatesRunnerInDestination() 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"); + } + + @Test + void runnerNameAndLibrariesDirectoriesCanBeCustomized() throws IOException { + run(ExtractCommandTests.this.archive, "--runner-filename", "runner-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(attributes).containsEntry("Class-Path", + "dependencies/dependency-1.jar dependencies/dependency-2.jar dependencies/dependency-3-SNAPSHOT.jar"); + } + + @Test + void runnerContainsManifestEntries() throws IOException { + run(ExtractCommandTests.this.archive); + File runner = file("runner.jar"); + Map attributes = getJarManifestAttributes(runner); + 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") + .doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib", + "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index"); + } + + @Test + void runnerContainsApplicationClassesAndResources() throws IOException { + run(ExtractCommandTests.this.archive); + File runner = file("runner.jar"); + List entryNames = getJarEntryNames(runner); + 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() + .satisfies(ExtractCommandTests.this::timeAttributes); + } + + @Test + void runnerDoesntContainLibraries() throws IOException { + run(ExtractCommandTests.this.archive); + File runner = file("runner.jar"); + List entryNames = getJarEntryNames(runner); + assertThat(entryNames).doesNotContain("BOOT-INF/lib/dependency-1.jar", "BOOT-INF/lib/dependency-2.jar"); + } + + @Test + void failsOnIncompatibleJar() throws IOException { + File file = file("empty.jar"); + try (FileWriter writer = new FileWriter(file)) { + writer.write("text"); + } + assertThatIllegalStateException().isThrownBy(() -> run(file)).withMessageContaining("not compatible"); + } + + } + + @Nested + class ExtractWithLayers { + + @Test + void extractLibrariesAndCreatesRunner() 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"); + } + + @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"); + } + + @Test + void printErrorIfLayersAreNotEnabled() throws IOException { + File archive = createArchive(); + TestPrintStream out = run(archive, "--layers"); + assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt"); + } + + } + + @Nested + class ExtractLauncher { + + @Test + 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"); + } + + @Test + void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws Exception { + File file = createArchive("e/../../e.jar", null); + assertThatIllegalStateException().isThrownBy(() -> run(file, "--launcher")) + .withMessageContaining("Entry 'e/../../e.jar' would be written"); + } + + } + + @Nested + class ExtractLauncherWithLayers { + + @Test + 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"); + } + + @Test + void printErrorIfLayersAreNotEnabled() throws IOException { + File archive = createArchive(); + TestPrintStream out = run(archive, "--launcher", "--layers"); + assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt"); + } + + @Test + 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index 319e59c69af..bf77b76a054 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.FileOutputStream; @@ -54,13 +54,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; /** - * Tests for {@link ExtractCommand}. + * Tests for {@link ExtractLayersCommand}. * * @author Phillip Webb * @author Andy Wilkinson */ @ExtendWith(MockitoExtension.class) -class ExtractCommandTests { +class ExtractLayersCommandTests { private static final Instant NOW = Instant.now(); @@ -82,21 +82,21 @@ class ExtractCommandTests { private final Layers layers = new TestLayers(); - private ExtractCommand command; + private ExtractLayersCommand command; @BeforeEach void setup() throws Exception { this.jarFile = createJarFile("test.jar"); this.extract = new File(this.temp, "extract"); this.extract.mkdir(); - this.command = new ExtractCommand(this.context, this.layers); + this.command = new ExtractLayersCommand(this.context, this.layers); } @Test void runExtractsLayers() { given(this.context.getArchiveFile()).willReturn(this.jarFile); given(this.context.getWorkingDir()).willReturn(this.extract); - this.command.run(Collections.emptyMap(), Collections.emptyList()); + this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()); assertThat(this.extract.list()).containsOnly("a", "b", "c", "d"); assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes); assertThat(new File(this.extract, "b/b/b.jar")).exists().satisfies(this::timeAttributes); @@ -145,7 +145,8 @@ class ExtractCommandTests { void runWhenHasDestinationOptionExtractsLayers() { given(this.context.getArchiveFile()).willReturn(this.jarFile); File out = new File(this.extract, "out"); - this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()), + this.command.run(System.out, + Collections.singletonMap(ExtractLayersCommand.DESTINATION_OPTION, out.getAbsolutePath()), Collections.emptyList()); assertThat(this.extract.list()).containsOnly("out"); assertThat(new File(this.extract, "out/a/a/a.jar")).exists().satisfies(this::timeAttributes); @@ -157,7 +158,7 @@ class ExtractCommandTests { void runWhenHasLayerParamsExtractsLimitedLayers() { given(this.context.getArchiveFile()).willReturn(this.jarFile); given(this.context.getWorkingDir()).willReturn(this.extract); - this.command.run(Collections.emptyMap(), Arrays.asList("a", "c")); + this.command.run(System.out, Collections.emptyMap(), Arrays.asList("a", "c")); assertThat(this.extract.list()).containsOnly("a", "c"); assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes); assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes); @@ -171,10 +172,9 @@ class ExtractCommandTests { writer.write("text"); } given(this.context.getArchiveFile()).willReturn(file); - given(this.context.getWorkingDir()).willReturn(this.extract); assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList())) - .withMessageContaining("not compatible with layertools"); + .isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList())) + .withMessageContaining("not compatible"); } @Test @@ -191,7 +191,7 @@ class ExtractCommandTests { given(this.context.getArchiveFile()).willReturn(this.jarFile); given(this.context.getWorkingDir()).willReturn(this.extract); assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList())) + .isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList())) .withMessageContaining("Entry 'e/../../e.jar' would be written"); } @@ -247,16 +247,21 @@ class ExtractCommandTests { } @Override - public String getLayer(ZipEntry entry) { - if (entry.getName().startsWith("a")) { + public String getLayer(String entryName) { + if (entryName.startsWith("a")) { return "a"; } - if (entry.getName().startsWith("b")) { + if (entryName.startsWith("b")) { return "b"; } return "c"; } + @Override + public String getApplicationLayerName() { + return "application"; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java new file mode 100644 index 00000000000..80291888a1c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link HelpCommand}. + * + * @author Phillip Webb + */ +class HelpCommandTests { + + private HelpCommand command; + + private TestPrintStream out; + + @TempDir + Path temp; + + @BeforeEach + void setup() { + Context context = Mockito.mock(Context.class); + given(context.getArchiveFile()).willReturn(this.temp.resolve("test.jar").toFile()); + this.command = new HelpCommand(context, List.of(new TestCommand()), "tools"); + this.out = new TestPrintStream(this); + } + + @Test + void shouldPrintAllCommands() { + this.command.run(this.out, Collections.emptyList()); + assertThat(this.out).hasSameContentAsResource("help-output.txt"); + } + + @Test + void shouldPrintCommandSpecificHelp() { + this.command.run(this.out, List.of("test")); + System.out.println(this.out); + assertThat(this.out).hasSameContentAsResource("help-test-output.txt"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java new file mode 100644 index 00000000000..d3f6387f8fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.jarmode.tools.JarStructure.Entry; +import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IndexedJarStructure}. + * + * @author Moritz Halbritter + */ +class IndexedJarStructureTests { + + @Test + void shouldResolveLibraryEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar"); + assertThat(entry.location()).isEqualTo("spring-webmvc-6.1.4.jar"); + assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/lib/spring-webmvc-6.1.4.jar"); + assertThat(entry.type()).isEqualTo(Type.LIBRARY); + } + + @Test + void shouldResolveApplicationEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/classes/application.properties"); + assertThat(entry.location()).isEqualTo("application.properties"); + assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/classes/application.properties"); + assertThat(entry.type()).isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE); + } + + @Test + void shouldResolveLoaderEntry() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.location()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.originalLocation()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher"); + assertThat(entry.type()).isEqualTo(Type.LOADER); + } + + @Test + void shouldNotResolveNonExistingLibs() throws IOException { + IndexedJarStructure structure = createStructure(); + Entry entry = structure.resolve("BOOT-INF/lib/doesnt-exists.jar"); + assertThat(entry).isNull(); + } + + @Test + void shouldCreateLauncherManifest() throws IOException { + IndexedJarStructure structure = createStructure(); + Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity()); + Map attributes = getAttributes(manifest); + assertThat(attributes).containsEntry("Manifest-Version", "1.0") + .containsEntry("Implementation-Title", "IndexedJarStructureTests") + .containsEntry("Spring-Boot-Version", "3.3.0-SNAPSHOT") + .containsEntry("Implementation-Version", "0.0.1-SNAPSHOT") + .containsEntry("Build-Jdk-Spec", "17") + .containsEntry("Class-Path", + "spring-webmvc-6.1.4.jar spring-web-6.1.4.jar spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar spring-boot-3.3.0-SNAPSHOT.jar jakarta.annotation-api-2.1.1.jar spring-context-6.1.4.jar spring-aop-6.1.4.jar spring-beans-6.1.4.jar spring-expression-6.1.4.jar spring-core-6.1.4.jar snakeyaml-2.2.jar jackson-datatype-jdk8-2.16.1.jar jackson-datatype-jsr310-2.16.1.jar jackson-module-parameter-names-2.16.1.jar jackson-databind-2.16.1.jar tomcat-embed-websocket-10.1.19.jar tomcat-embed-core-10.1.19.jar tomcat-embed-el-10.1.19.jar micrometer-observation-1.13.0-M1.jar logback-classic-1.4.14.jar log4j-to-slf4j-2.23.0.jar jul-to-slf4j-2.0.12.jar spring-jcl-6.1.4.jar jackson-annotations-2.16.1.jar jackson-core-2.16.1.jar micrometer-commons-1.13.0-M1.jar logback-core-1.4.14.jar slf4j-api-2.0.12.jar log4j-api-2.23.0.jar") + .containsEntry("Main-Class", "org.springframework.boot.jarmode.tools.IndexedJarStructureTests") + .doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib", "Spring-Boot-Classpath-Index", + "Spring-Boot-Layers-Index"); + } + + @Test + void shouldLoadFromFile(@TempDir File tempDir) throws IOException { + File jarFile = new File(tempDir, "test.jar"); + try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(jarFile), createManifest())) { + outputStream.putNextEntry(new ZipEntry("BOOT-INF/classpath.idx")); + outputStream.write(createIndexFile().getBytes(StandardCharsets.UTF_8)); + outputStream.closeEntry(); + } + IndexedJarStructure structure = IndexedJarStructure.get(jarFile); + assertThat(structure).isNotNull(); + assertThat(structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar")).extracting(Entry::type) + .isEqualTo(Type.LIBRARY); + assertThat(structure.resolve("BOOT-INF/classes/application.properties")).extracting(Entry::type) + .isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE); + } + + private Map getAttributes(Manifest manifest) { + Map result = new HashMap<>(); + manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString())); + return result; + } + + private IndexedJarStructure createStructure() throws IOException { + return new IndexedJarStructure(createManifest(), createIndexFile()); + } + + private String createIndexFile() { + return """ + - "BOOT-INF/lib/spring-webmvc-6.1.4.jar" + - "BOOT-INF/lib/spring-web-6.1.4.jar" + - "BOOT-INF/lib/spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar" + - "BOOT-INF/lib/spring-boot-3.3.0-SNAPSHOT.jar" + - "BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar" + - "BOOT-INF/lib/spring-context-6.1.4.jar" + - "BOOT-INF/lib/spring-aop-6.1.4.jar" + - "BOOT-INF/lib/spring-beans-6.1.4.jar" + - "BOOT-INF/lib/spring-expression-6.1.4.jar" + - "BOOT-INF/lib/spring-core-6.1.4.jar" + - "BOOT-INF/lib/snakeyaml-2.2.jar" + - "BOOT-INF/lib/jackson-datatype-jdk8-2.16.1.jar" + - "BOOT-INF/lib/jackson-datatype-jsr310-2.16.1.jar" + - "BOOT-INF/lib/jackson-module-parameter-names-2.16.1.jar" + - "BOOT-INF/lib/jackson-databind-2.16.1.jar" + - "BOOT-INF/lib/tomcat-embed-websocket-10.1.19.jar" + - "BOOT-INF/lib/tomcat-embed-core-10.1.19.jar" + - "BOOT-INF/lib/tomcat-embed-el-10.1.19.jar" + - "BOOT-INF/lib/micrometer-observation-1.13.0-M1.jar" + - "BOOT-INF/lib/logback-classic-1.4.14.jar" + - "BOOT-INF/lib/log4j-to-slf4j-2.23.0.jar" + - "BOOT-INF/lib/jul-to-slf4j-2.0.12.jar" + - "BOOT-INF/lib/spring-jcl-6.1.4.jar" + - "BOOT-INF/lib/jackson-annotations-2.16.1.jar" + - "BOOT-INF/lib/jackson-core-2.16.1.jar" + - "BOOT-INF/lib/micrometer-commons-1.13.0-M1.jar" + - "BOOT-INF/lib/logback-core-1.4.14.jar" + - "BOOT-INF/lib/slf4j-api-2.0.12.jar" + - "BOOT-INF/lib/log4j-api-2.23.0.jar" + """; + } + + private Manifest createManifest() throws IOException { + return new Manifest(new ByteArrayInputStream(""" + Manifest-Version: 1.0 + Main-Class: org.springframework.boot.loader.launch.JarLauncher + Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests + Spring-Boot-Version: 3.3.0-SNAPSHOT + Spring-Boot-Classes: BOOT-INF/classes/ + Spring-Boot-Lib: BOOT-INF/lib/ + Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx + Spring-Boot-Layers-Index: BOOT-INF/layers.idx + Build-Jdk-Spec: 17 + Implementation-Title: IndexedJarStructureTests + Implementation-Version: 0.0.1-SNAPSHOT + """.getBytes(StandardCharsets.UTF_8))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java similarity index 88% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java index 9b34b4cd304..9c543ca6b25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.FileOutputStream; @@ -46,46 +46,46 @@ class IndexedLayersTests { @Test void createWhenIndexFileIsEmptyThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ")) + assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ", "BOOT-INF/classes")) .withMessage("Empty layer index file loaded"); } @Test void createWhenIndexFileIsMalformedThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test")) + assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test", "BOOT-INF/classes")) .withMessage("Layer index file is malformed"); } @Test void iteratorReturnsLayers() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers).containsExactly("test", "empty", "application"); } @Test void getLayerWhenMatchesNameReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test"); assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application"); } @Test void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar"))) .withMessage("No layer defined in index for file " + "'file.jar'"); } @Test void getLayerWhenMatchesDirectoryReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); assertThat(layers.getLayer(mockEntry("META-INF/a/sub/directory/and/a/file"))).isEqualTo("application"); } @Test void getLayerWhenFileHasSpaceReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java similarity index 83% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java index a8c57607516..7abc558b25d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.FileOutputStream; @@ -62,43 +62,45 @@ class LayerToolsJarModeTests { this.out = new TestPrintStream(this); this.systemOut = System.out; System.setOut(this.out); - LayerToolsJarMode.Runner.contextOverride = context; + LayerToolsJarMode.contextOverride = context; + System.setProperty("jarmode", "layertools"); } @AfterEach void restore() { System.setOut(this.systemOut); - LayerToolsJarMode.Runner.contextOverride = null; + LayerToolsJarMode.contextOverride = null; + System.clearProperty("jarmode"); } @Test void mainWithNoParametersShowsHelp() { new LayerToolsJarMode().run("layertools", NO_ARGS); - assertThat(this.out).hasSameContentAsResource("help-output.txt"); + assertThat(this.out).hasSameContentAsResource("layertools-help-output.txt"); } @Test void mainWithArgRunsCommand() { new LayerToolsJarMode().run("layertools", new String[] { "list" }); - assertThat(this.out).hasSameContentAsResource("list-output.txt"); + assertThat(this.out).hasSameContentAsResource("layertools-list-output.txt"); } @Test void mainWithUnknownCommandShowsErrorAndHelp() { new LayerToolsJarMode().run("layertools", new String[] { "invalid" }); - assertThat(this.out).hasSameContentAsResource("error-command-unknown-output.txt"); + assertThat(this.out).hasSameContentAsResource("layertools-error-command-unknown-output.txt"); } @Test void mainWithUnknownOptionShowsErrorAndCommandHelp() { new LayerToolsJarMode().run("layertools", new String[] { "extract", "--invalid" }); - assertThat(this.out).hasSameContentAsResource("error-option-unknown-output.txt"); + assertThat(this.out).hasSameContentAsResource("layertools-error-option-unknown-output.txt"); } @Test void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() { new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" }); - assertThat(this.out).hasSameContentAsResource("error-option-missing-value-output.txt"); + assertThat(this.out).hasSameContentAsResource("layertools-error-option-missing-value-output.txt"); } private File createJarFile(String name) throws Exception { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java index 91856efecc2..c8b1dc53b24 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; @@ -35,7 +34,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.ClassPathResource; -import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -55,16 +53,14 @@ class ListCommandTests { @Mock private Context context; - private File jarFile; - private ListCommand command; private TestPrintStream out; @BeforeEach void setup() throws Exception { - this.jarFile = createJarFile("test.jar"); - given(this.context.getArchiveFile()).willReturn(this.jarFile); + File jarFile = createJarFile("test.jar"); + given(this.context.getArchiveFile()).willReturn(jarFile); this.command = new ListCommand(this.context); this.out = new TestPrintStream(this); } @@ -73,7 +69,7 @@ class ListCommandTests { void listLayersShouldListLayers() { Layers layers = IndexedLayers.get(this.context); this.command.printLayers(layers, this.out); - assertThat(this.out).hasSameContentAsResource("list-output.txt"); + assertThat(this.out).hasSameContentAsResource("list-output-without-deprecation.txt"); } private File createJarFile(String name) throws Exception { @@ -117,9 +113,7 @@ class ListCommandTests { } private String getFile(String fileName) throws Exception { - ClassPathResource resource = new ClassPathResource(fileName, getClass()); - InputStreamReader reader = new InputStreamReader(resource.getInputStream()); - return FileCopyUtils.copyToString(reader); + return new ClassPathResource(fileName, getClass()).getContentAsString(StandardCharsets.UTF_8); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java new file mode 100644 index 00000000000..4b338a6b3e3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.File; +import java.io.IOException; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ListLayersCommand}. + * + * @author Moritz Halbritter + */ +class ListLayersCommandTests extends AbstractTests { + + @Test + void shouldListLayers() throws IOException { + Manifest manifest = createManifest("Spring-Boot-Layers-Index: META-INF/layers.idx"); + TestPrintStream out = run(createArchive(manifest, "META-INF/layers.idx", "/jar-contents/layers.idx")); + assertThat(out).hasSameContentAsResource("list-layers-output.txt"); + } + + @Test + void shouldPrintErrorWhenLayersAreNotEnabled() throws IOException { + TestPrintStream out = run(createArchive()); + assertThat(out).hasSameContentAsResource("list-layers-output-layers-disabled.txt"); + } + + private TestPrintStream run(File archive) { + return runCommand(ListLayersCommand::new, archive); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java new file mode 100644 index 00000000000..c8d5aba2c97 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.PrintStream; +import java.util.List; +import java.util.Map; + +/** + * @author Moritz Halbritter + */ +class TestCommand extends Command { + + TestCommand() { + super("test", "Description of test", + Options.of(Option.of("option1", "value1", "Description of option1"), + Option.of("option2", "value2", "Description of option2")), + Parameters.of("parameter1", "parameter2")); + } + + @Override + protected void run(PrintStream out, Map options, List parameters) { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java index f3d1beb43f8..ce4bd67b8a4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.jarmode.layertools; +package org.springframework.boot.jarmode.tools; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -27,7 +27,7 @@ import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AssertProvider; import org.assertj.core.api.Assertions; -import org.springframework.boot.jarmode.layertools.TestPrintStream.PrintStreamAssert; +import org.springframework.boot.jarmode.tools.TestPrintStream.PrintStreamAssert; import org.springframework.util.FileCopyUtils; /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java new file mode 100644 index 00000000000..135a305dc44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.tools; + +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ToolsJarMode}. + * + * @author Moritz Halbritter + */ +class ToolsJarModeTests extends AbstractTests { + + private ToolsJarMode mode; + + private TestPrintStream out; + + @BeforeEach + void setUp() throws IOException { + this.out = new TestPrintStream(this); + Context context = new Context(createArchive(), this.tempDir); + this.mode = new ToolsJarMode(context, this.out); + } + + @Test + void shouldAcceptToolsMode() { + assertThat(this.mode.accepts("tools")).isTrue(); + assertThat(this.mode.accepts("something-else")).isFalse(); + } + + @Test + void noParametersShowsHelp() { + run(); + assertThat(this.out).hasSameContentAsResource("tools-help-output.txt"); + } + + @Test + void helpForExtract() { + run("help", "extract"); + assertThat(this.out).hasSameContentAsResource("tools-help-extract-output.txt"); + } + + @Test + void helpForListLayers() { + run("help", "list-layers"); + assertThat(this.out).hasSameContentAsResource("tools-help-list-layers-output.txt"); + } + + @Test + void helpForHelp() { + run("help", "help"); + assertThat(this.out).hasSameContentAsResource("tools-help-help-output.txt"); + } + + @Test + void helpForUnknownCommand() { + run("help", "unknown-command"); + assertThat(this.out).hasSameContentAsResource("tools-help-unknown-command-output.txt"); + } + + @Test + void unknownCommandShowsErrorAndHelp() { + run("something-invalid"); + assertThat(this.out).hasSameContentAsResource("tools-error-command-unknown-output.txt"); + } + + @Test + void unknownOptionShowsErrorAndCommandHelp() { + run("extract", "--something-invalid"); + assertThat(this.out).hasSameContentAsResource("tools-error-option-unknown-output.txt"); + } + + @Test + void optionMissingRequiredValueShowsErrorAndCommandHelp() { + run("extract", "--destination"); + assertThat(this.out).hasSameContentAsResource("tools-error-option-missing-value-output.txt"); + } + + private void run(String... args) { + this.mode.run("tools", args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties new file mode 100644 index 00000000000..15edec8e3d7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties @@ -0,0 +1 @@ +spring.application.name=test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx new file mode 100644 index 00000000000..dac08add8aa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx @@ -0,0 +1,3 @@ +- "BOOT-INF/lib/dependency-1.jar" +- "BOOT-INF/lib/dependency-2.jar" +- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1 b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2 b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx new file mode 100644 index 00000000000..09c3d320f1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx @@ -0,0 +1,12 @@ +- "dependencies": + - "BOOT-INF/lib/dependency-1.jar" + - "BOOT-INF/lib/dependency-2.jar" +- "spring-boot-loader": + - "org/" +- "snapshot-dependencies": + - "BOOT-INF/lib/dependency-3-SNAPSHOT.jar" +- "application": + - "BOOT-INF/classes/" + - "BOOT-INF/classpath.idx" + - "BOOT-INF/layers.idx" + - "META-INF/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt new file mode 100644 index 00000000000..994354551a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt @@ -0,0 +1,2 @@ +Error: Layers are not enabled + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt new file mode 100644 index 00000000000..69c78f5a375 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt @@ -0,0 +1,6 @@ +Usage: + java -Djarmode=tools -jar test.jar + +Available commands: + test Description of test + help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt new file mode 100644 index 00000000000..eb7a19d113f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt @@ -0,0 +1,8 @@ +Description of test + +Usage: + java -Djarmode=tools -jar test.jar test [options] parameter1 parameter2 + +Options: + --option1 value1 Description of option1 + --option2 value2 Description of option2 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt index 20a2ca2098d..425ac1025e3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt @@ -4,6 +4,7 @@ Usage: java -Djarmode=layertools -jar test.jar Available commands: + help Help about any command +Deprecated commands: list List layers from the jar that can be extracted extract Extracts layers from the jar for image creation - help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt similarity index 73% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt index 6a5034cedd7..18e87069ce0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt @@ -1,3 +1,5 @@ +Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. + Error: Option "--destination" for the extract command requires a value Extracts layers from the jar for image creation diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt similarity index 72% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt index a207b3b20a2..3b832dd642e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt @@ -1,3 +1,5 @@ +Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. + Error: Unknown option "--invalid" for the extract command Extracts layers from the jar for image creation diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt index 47d5f4b3ba9..daea473a9b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt @@ -2,6 +2,7 @@ Usage: java -Djarmode=layertools -jar test.jar Available commands: + help Help about any command +Deprecated commands: list List layers from the jar that can be extracted extract Extracts layers from the jar for image creation - help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt new file mode 100644 index 00000000000..03f7f88556f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt @@ -0,0 +1,5 @@ +Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead. + +0001 +0002 +0003 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt new file mode 100644 index 00000000000..994354551a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt @@ -0,0 +1,2 @@ +Error: Layers are not enabled + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt new file mode 100644 index 00000000000..8a1e6d4dcc6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt @@ -0,0 +1,4 @@ +dependencies +spring-boot-loader +snapshot-dependencies +application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-manifest.MF b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-manifest.MF rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-layers.idx rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-manifest.MF b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-manifest.MF rename to spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt new file mode 100644 index 00000000000..069368999c6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt @@ -0,0 +1,9 @@ +Error: Unknown command "something-invalid" + +Usage: + java -Djarmode=tools -jar test.jar + +Available commands: + extract Extract the contents from the jar + list-layers List layers from the jar that can be extracted + help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt new file mode 100644 index 00000000000..19dd4f72ada --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt @@ -0,0 +1,13 @@ +Error: Option "--destination" for the extract command requires a value + +Extract the contents from the jar + +Usage: + java -Djarmode=tools -jar test.jar extract [options] + +Options: + --launcher Whether to extract the Spring Boot launcher + --layers string list Layers to extract + --destination string Directory to extract files to. Defaults to the current working directory + --libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/ + --runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt new file mode 100644 index 00000000000..9ea8f884ae4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt @@ -0,0 +1,13 @@ +Error: Unknown option "--something-invalid" for the extract command + +Extract the contents from the jar + +Usage: + java -Djarmode=tools -jar test.jar extract [options] + +Options: + --launcher Whether to extract the Spring Boot launcher + --layers string list Layers to extract + --destination string Directory to extract files to. Defaults to the current working directory + --libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/ + --runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt new file mode 100644 index 00000000000..514fe294155 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt @@ -0,0 +1,11 @@ +Extract the contents from the jar + +Usage: + java -Djarmode=tools -jar test.jar extract [options] + +Options: + --launcher Whether to extract the Spring Boot launcher + --layers string list Layers to extract + --destination string Directory to extract files to. Defaults to the current working directory + --libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/ + --runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt new file mode 100644 index 00000000000..8b5187e7679 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt @@ -0,0 +1,4 @@ +Help about any command + +Usage: + java -Djarmode=tools -jar test.jar help [] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt new file mode 100644 index 00000000000..d97934afca7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt @@ -0,0 +1,4 @@ +List layers from the jar that can be extracted + +Usage: + java -Djarmode=tools -jar test.jar list-layers diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt new file mode 100644 index 00000000000..8d29d30374b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-output.txt @@ -0,0 +1,7 @@ +Usage: + java -Djarmode=tools -jar test.jar + +Available commands: + extract Extract the contents from the jar + list-layers List layers from the jar that can be extracted + help Help about any command diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt new file mode 100644 index 00000000000..f31247aea6c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-unknown-command-output.txt @@ -0,0 +1,9 @@ +Error: Unknown command "unknown-command" + +Usage: + java -Djarmode=tools -jar test.jar + +Available commands: + extract Extract the contents from the jar + list-layers List layers from the jar that can be extracted + help Help about any command 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 f7968f659d5..c2e3330517d 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 @@ -43,7 +43,7 @@ dependencies { 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")) + jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-tools")) testImplementation("org.assertj:assertj-core") testImplementation("org.junit.jupiter:junit-jupiter") @@ -81,18 +81,18 @@ task reproducibleLoaderClassicJar(type: Jar) { destinationDirectory = file("${generatedResources}/META-INF/loader") } -task layerToolsJar(type: Sync) { +task toolsJar(type: Sync) { dependsOn configurations.jarmode from { file(configurations.jarmode.incoming.files.singleFile) } - rename({ "spring-boot-jarmode-layertools.jar" }) + rename({ "spring-boot-jarmode-tools.jar" }) into(file("${generatedResources}/META-INF/jarmode")) } sourceSets { main { - output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) + output.dir(generatedResources, builtBy: [toolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java index 49508ab5187..ef6199f6713 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public class JarModeLibrary extends Library { /** * {@link JarModeLibrary} for layer tools. */ - public static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools"); + public static final JarModeLibrary TOOLS = new JarModeLibrary("spring-boot-jarmode-tools"); JarModeLibrary(String artifactId) { this(createCoordinates(artifactId)); 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 8a66fa186aa..0fa33c64de6 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 @@ -511,8 +511,8 @@ public abstract class Packager { addLibrary(library); } }); - if (isLayered() && Packager.this.includeRelevantJarModeJars) { - addLibrary(JarModeLibrary.LAYER_TOOLS); + if (Packager.this.includeRelevantJarModeJars) { + addLibrary(JarModeLibrary.TOOLS); } this.unpackHandler = new PackagedLibrariesUnpackHandler(); this.libraryLookup = this::lookup; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 7dce78edafc..7d1ff71ce25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -226,6 +226,7 @@ abstract class AbstractPackagerTests

{ this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File file = this.testJarFile.getFile(); P packager = createPackager(file); + packager.setIncludeRelevantJarModeJars(false); execute(packager, (callback) -> { callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false)); callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false)); @@ -299,7 +300,7 @@ abstract class AbstractPackagerTests

{ assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx"); assertThat(Arrays.asList(classpathIndex.split("\\n"))) - .containsExactly("- \"BOOT-INF/lib/spring-boot-jarmode-layertools.jar\""); + .containsExactly("- \"BOOT-INF/lib/spring-boot-jarmode-tools.jar\""); assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); List expectedLayers = new ArrayList<>(); @@ -606,6 +607,7 @@ abstract class AbstractPackagerTests

{ this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class); this.testJarFile.addFile("WEB-INF/lib/" + webLibrary.getName(), webLibrary); P packager = createPackager(this.testJarFile.getFile("war")); + packager.setIncludeRelevantJarModeJars(false); packager.setLayout(new Layouts.War()); execute(packager, (callback) -> { callback.library(newLibrary(webLibrary, LibraryScope.COMPILE, false, false)); 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 d96b959fe64..d15caedf313 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 @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Andy Wilkinson * @author Madhura Bhave * @author Scott Frederick + * @author Moritz Halbritter */ @ExtendWith(MavenBuildExtension.class) class JarIntegrationTests extends AbstractArchiveIntegrationTests { @@ -337,8 +338,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") - .hasEntryWithNameStartingWith( - "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + .hasEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); try (JarFile jarFile = new JarFile(repackaged)) { Map> layerIndex = readLayerIndex(jarFile); assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", @@ -361,8 +361,8 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") - .doesNotHaveEntryWithName("BOOT-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .hasEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()) + .doesNotHaveEntryWithName("BOOT-INF/layers.idx"); }); } @@ -374,7 +374,20 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") .hasEntryWithNameStartingWith("BOOT-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .doesNotHaveEntryWithNameStartingWith( + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithToolsExclude(MavenBuild mavenBuild) { + mavenBuild.project("jar-no-tools").execute((project) -> { + File repackaged = new File(project, "jar/target/jar-no-tools-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot") + .doesNotHaveEntryWithNameStartingWith( + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); }); } @@ -451,7 +464,8 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { File repackaged = new File(project, "target/jar-output-timestamp-0.0.1.BUILD-SNAPSHOT.jar"); List sortedLibs = Arrays.asList("BOOT-INF/lib/jakarta.servlet-api", "BOOT-INF/lib/micrometer-commons", "BOOT-INF/lib/micrometer-observation", "BOOT-INF/lib/spring-aop", - "BOOT-INF/lib/spring-beans", "BOOT-INF/lib/spring-boot-jarmode-layertools", + "BOOT-INF/lib/spring-beans", + "BOOT-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId(), "BOOT-INF/lib/spring-context", "BOOT-INF/lib/spring-core", "BOOT-INF/lib/spring-expression", "BOOT-INF/lib/spring-jcl"); assertThat(jar(repackaged)).entryNamesInPath("BOOT-INF/lib/") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java index 56a266b5bfd..d9038197cbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson * @author Scott Frederick + * @author Moritz Halbritter */ @ExtendWith(MavenBuildExtension.class) class WarIntegrationTests extends AbstractArchiveIntegrationTests { @@ -127,7 +128,7 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { "WEB-INF/lib/spring-expression", "WEB-INF/lib/spring-jcl", // these libraries are contributed by Spring Boot repackaging, and // sorted separately - "WEB-INF/lib/spring-boot-jarmode-layertools"); + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); assertThat(jar(repackaged)).entryNamesInPath("WEB-INF/lib/") .zipSatisfy(sortedLibs, (String jarLib, String expectedLib) -> assertThat(jarLib).startsWith(expectedLib)); @@ -150,8 +151,7 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") - .hasEntryWithNameStartingWith( - "WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + .hasEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); try (JarFile jarFile = new JarFile(repackaged)) { Map> layerIndex = readLayerIndex(jarFile); assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", @@ -179,8 +179,8 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") - .doesNotHaveEntryWithName("WEB-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .hasEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()) + .doesNotHaveEntryWithName("WEB-INF/layers.idx"); }); } @@ -192,7 +192,20 @@ class WarIntegrationTests extends AbstractArchiveIntegrationTests { .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") .hasEntryWithNameStartingWith("WEB-INF/layers.idx") - .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + .doesNotHaveEntryWithNameStartingWith( + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); + }); + } + + @TestTemplate + void whenWarIsRepackagedWithToolsExclude(MavenBuild mavenBuild) { + mavenBuild.project("war-no-tools").execute((project) -> { + File repackaged = new File(project, "war/target/war-no-tools-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") + .doesNotHaveEntryWithNameStartingWith( + "WEB-INF/lib/" + JarModeLibrary.TOOLS.getCoordinates().getArtifactId()); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml new file mode 100644 index 00000000000..a06fe545f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml new file mode 100644 index 00000000000..ab31e719baf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml new file mode 100644 index 00000000000..beb9e4c68e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-no-tools + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..1277cdbc5a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml new file mode 100644 index 00000000000..fdd98953811 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-no-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml new file mode 100644 index 00000000000..a06fe545f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml new file mode 100644 index 00000000000..ab31e719baf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml new file mode 100644 index 00000000000..60503bdf68f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml new file mode 100644 index 00000000000..fc478ed1111 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-no-tools + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..1277cdbc5a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html new file mode 100644 index 00000000000..18ecdcb795c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-no-tools/war/src/main/webapp/index.html @@ -0,0 +1 @@ + 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 28d55d213a1..3fbfe48e710 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,7 @@ import org.springframework.boot.loader.tools.layer.CustomLayers; * * @author Phillip Webb * @author Scott Frederick + * @author Moritz Halbritter * @since 2.3.0 */ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo { @@ -112,13 +113,20 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo @Parameter(defaultValue = "false") public boolean includeSystemScope; + /** + * Include JAR tools. + * @since 3.3.0 + */ + @Parameter(defaultValue = "true") + public boolean includeTools = true; + /** * Layer configuration with options to disable layer creation, exclude layer tools * jar, and provide a custom layers configuration file. * @since 2.3.0 */ @Parameter - private Layers layers; + private Layers layers = new Layers(); /** * Return the type of archive that should be packaged by this MOJO. @@ -164,17 +172,22 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo getLog().info("Layout: " + layout); packager.setLayout(layout.layout()); } - if (this.layers == null) { - packager.setLayers(IMPLICIT_LAYERS); - } - else if (this.layers.isEnabled()) { + if (this.layers.isEnabled()) { packager.setLayers((this.layers.getConfiguration() != null) ? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS); - packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools()); } + packager.setIncludeRelevantJarModeJars(getIncludeRelevantJarModeJars()); return packager; } + @SuppressWarnings("removal") + private boolean getIncludeRelevantJarModeJars() { + if (!this.includeTools) { + return false; + } + return this.layers.isIncludeLayerTools(); + } + private CustomLayers getCustomLayers(File configuration) { try { Document document = getDocumentIfAvailable(configuration); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index ce981b4248f..cb4d6a235d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ public class Layers { private boolean enabled = true; + @Deprecated(since = "3.3.0", forRemoval = true) private boolean includeLayerTools = true; private File configuration; @@ -43,7 +44,9 @@ public class Layers { /** * Whether to include the layer tools jar. * @return true if layer tools should be included + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of {@code includeTools}. */ + @Deprecated(since = "3.3.0", forRemoval = true) public boolean isIncludeLayerTools() { return this.includeLayerTools; }