Implement extract and list-layers command

Adds a new jarmode called 'tools'. This provides two commands,
'extract' and 'list-layers'. list-layers is the same as list from
the layertools.

extract is able to extract the JAR in four different modes:

- CDS compatible extraction with libraries in a lib folder and a runner
.jar
- CDS compatible as above, but with layers
- Launcher based
- Launcher based with layers. This is essentially the same as extract
  from the layertools

The commands in layertools have been deprecated in favor of the commands
in 'tools'.

This also changes the behavior of layers.enabled from the Gradle and
Maven plugin: before this commit, layers.enabled prevents the inclusion
of the layer index file as well as the layertools JAR.
After this commit, layers.enabled only prevents the inclusion of the
layer index file.

layer.includeLayerTools have been deprecated in favor of includeTools,
and the layertools JAR has been renamed to tools.

Closes gh-38276
This commit is contained in:
Moritz Halbritter
2024-02-09 15:04:46 +01:00
parent 2c4fb5baaa
commit 793aca60d2
121 changed files with 2780 additions and 612 deletions

View File

@@ -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<String, String> getJarManifestAttributes(File jar) throws IOException {
assertThat(jar).exists();
Manifest manifest = getJarManifest(jar);
Map<String, String> result = new HashMap<>();
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
return result;
}
List<String> getJarEntryNames(File jar) throws IOException {
assertThat(jar).exists();
try (JarFile jarFile = new JarFile(jar)) {
return jarFile.stream().map(ZipEntry::getName).toList();
}
}
List<String> listFilenames() throws IOException {
return listFilenames(this.tempDir);
}
List<String> listFilenames(File directory) throws IOException {
try (Stream<Path> 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 extends Command> {
T create(Context context);
}
}

View File

@@ -0,0 +1,196 @@
/*
* 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.Arrays;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
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;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link Command}.
*
* @author Phillip Webb
* @author Scott Frederick
* @author Moritz Halbritter
*/
class CommandTests {
private static final Option VERBOSE_FLAG = Option.flag("verbose", "Verbose output");
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");
assertThat(command.getName()).isEqualTo("test");
}
@Test
void getDescriptionReturnsDescription() {
TestCommand command = new TestCommand("test", "Test description", Options.none(), Parameters.none());
assertThat(command.getDescription()).isEqualTo("Test description");
}
@Test
void getOptionsReturnsOptions() {
Options options = Options.of(LOG_LEVEL_OPTION);
TestCommand command = new TestCommand("test", "test", options, Parameters.none());
assertThat(command.getOptions()).isEqualTo(options);
}
@Test
void getParametersReturnsParameters() {
Parameters parameters = Parameters.of("[<param>]");
TestCommand command = new TestCommand("test", "test", Options.none(), parameters);
assertThat(command.getParameters()).isEqualTo(parameters);
}
@Test
void runWithOptionsAndParametersParsesOptionsAndParameters() {
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION);
run(command, "--verbose", "--log-level", "test1", "test2", "test3");
assertThat(command.getRunOptions()).containsEntry(VERBOSE_FLAG, null);
assertThat(command.getRunOptions()).containsEntry(LOG_LEVEL_OPTION, "test1");
assertThat(command.getRunParameters()).containsExactly("test2", "test3");
}
@Test
void runWithUnknownOptionThrowsException() {
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION);
assertThatExceptionOfType(UnknownOptionException.class).isThrownBy(() -> run(command, "--invalid"))
.withMessage("--invalid");
}
@Test
void runWithOptionMissingRequiredValueThrowsException() {
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION);
assertThatExceptionOfType(MissingValueException.class)
.isThrownBy(() -> run(command, "--verbose", "--log-level"))
.withMessage("--log-level");
}
@Test
void findWhenNameMatchesReturnsCommand() {
TestCommand test1 = new TestCommand("test1");
TestCommand test2 = new TestCommand("test2");
List<Command> commands = Arrays.asList(test1, test2);
assertThat(Command.find(commands, "test1")).isEqualTo(test1);
assertThat(Command.find(commands, "test2")).isEqualTo(test2);
}
@Test
void findWhenNameDoesNotMatchReturnsNull() {
TestCommand test1 = new TestCommand("test1");
TestCommand test2 = new TestCommand("test2");
List<Command> commands = Arrays.asList(test1, test2);
assertThat(Command.find(commands, "test3")).isNull();
}
@Test
void parametersOfCreatesParametersInstance() {
Parameters parameters = Parameters.of("test1", "test2");
assertThat(parameters.getDescriptions()).containsExactly("test1", "test2");
}
@Test
void optionsNoneReturnsEmptyOptions() {
Options options = Options.none();
assertThat(options).extracting("values", as(InstanceOfAssertFactories.ARRAY)).isEmpty();
}
@Test
void optionsOfReturnsOptions() {
Option option = Option.of("test", "value description", "description");
Options options = Options.of(option);
assertThat(options).extracting("values", as(InstanceOfAssertFactories.ARRAY)).containsExactly(option);
}
@Test
void optionFlagCreatesFlagOption() {
Option option = Option.flag("test", "description");
assertThat(option.getName()).isEqualTo("test");
assertThat(option.getDescription()).isEqualTo("description");
assertThat(option.getValueDescription()).isNull();
}
@Test
void optionOfCreatesValueOption() {
Option option = Option.of("test", "value description", "description");
assertThat(option.getName()).isEqualTo("test");
assertThat(option.getDescription()).isEqualTo("description");
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(System.out, new ArrayDeque<>(Arrays.asList(args)));
}
static class TestCommand extends Command {
private Map<Option, String> runOptions;
private List<String> runParameters;
TestCommand(String name, Option... options) {
this(name, "test", Options.of(options), Parameters.none());
}
TestCommand(String name, String description, Options options, Parameters parameters) {
super(name, description, options, parameters);
}
@Override
protected void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
this.runOptions = options;
this.runParameters = parameters;
}
Map<Option, String> getRunOptions() {
return this.runOptions;
}
List<String> getRunParameters() {
return this.runParameters;
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.nio.file.Files;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link Context}.
*
* @author Phillip Webb
*/
class ContextTests {
@TempDir
File temp;
@Test
void createWhenSourceIsNullThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new Context(null, this.temp))
.withMessage("Unable to find source archive");
}
@Test
void createWhenSourceIsDirectoryThrowsException() {
File directory = new File(this.temp, "test");
directory.mkdir();
assertThatIllegalStateException().isThrownBy(() -> new Context(directory, this.temp))
.withMessage("Unable to find source archive");
}
@Test
void createWhenSourceIsNotJarOrWarThrowsException() throws Exception {
File zip = new File(this.temp, "test.zip");
Files.createFile(zip.toPath());
assertThatIllegalStateException().isThrownBy(() -> new Context(zip, this.temp))
.withMessageContaining("test.zip must end with .jar or .war");
}
@Test
void getJarFileReturnsJar() throws Exception {
File jar = new File(this.temp, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getArchiveFile()).isEqualTo(jar);
}
@Test
void getWorkingDirectoryReturnsWorkingDir() throws IOException {
File jar = new File(this.temp, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getWorkingDir()).isEqualTo(this.temp);
}
@Test
void getRelativePathReturnsRelativePath() throws Exception {
File target = new File(this.temp, "target");
target.mkdir();
File jar = new File(target, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getRelativeArchiveDir()).isEqualTo("target");
}
@Test
void getRelativePathWhenWorkingDirReturnsNull() throws Exception {
File jar = new File(this.temp, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, this.temp);
assertThat(context.getRelativeArchiveDir()).isNull();
}
@Test
void getRelativePathWhenCannotBeDeducedReturnsNull() throws Exception {
File directory1 = new File(this.temp, "directory1");
directory1.mkdir();
File directory2 = new File(this.temp, "directory2");
directory2.mkdir();
File jar = new File(directory1, "test.jar");
Files.createFile(jar.toPath());
Context context = new Context(jar, directory2);
assertThat(context.getRelativeArchiveDir()).isNull();
}
}

View File

@@ -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<String> 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<String> 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<String> 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<String, String> 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<String, String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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");
}
}
}

View File

@@ -0,0 +1,267 @@
/*
* 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.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.Runtime.Version;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
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.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ExtractLayersCommand}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
@ExtendWith(MockitoExtension.class)
class ExtractLayersCommandTests {
private static final Instant NOW = Instant.now();
private static final FileTime CREATION_TIME = FileTime.from(NOW.minus(3, ChronoUnit.DAYS));
private static final FileTime LAST_MODIFIED_TIME = FileTime.from(NOW.minus(2, ChronoUnit.DAYS));
private static final FileTime LAST_ACCESS_TIME = FileTime.from(NOW.minus(1, ChronoUnit.DAYS));
@TempDir
File temp;
@Mock
private Context context;
private File jarFile;
private File extract;
private final Layers layers = new TestLayers();
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 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(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);
assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes);
assertThat(new File(this.extract, "d")).isDirectory();
assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist();
}
private void timeAttributes(File file) {
try {
BasicFileAttributes basicAttributes = Files
.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
.readAttributes();
assertThat(basicAttributes.lastModifiedTime().to(TimeUnit.SECONDS))
.isEqualTo(LAST_MODIFIED_TIME.to(TimeUnit.SECONDS));
FileTime expectedCreationTime = expectedCreationTime();
if (expectedCreationTime != null) {
assertThat(basicAttributes.creationTime().to(TimeUnit.SECONDS))
.isEqualTo(expectedCreationTime.to(TimeUnit.SECONDS));
}
assertThat(basicAttributes.lastAccessTime().to(TimeUnit.SECONDS))
.isEqualTo(LAST_ACCESS_TIME.to(TimeUnit.SECONDS));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private FileTime 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;
}
@Test
void runWhenHasDestinationOptionExtractsLayers() {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
File out = new File(this.extract, "out");
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);
assertThat(new File(this.extract, "out/b/b/b.jar")).exists().satisfies(this::timeAttributes);
assertThat(new File(this.extract, "out/c/c/c.jar")).exists().satisfies(this::timeAttributes);
}
@Test
void runWhenHasLayerParamsExtractsLimitedLayers() {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
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);
assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist();
}
@Test
void runWithJarFileContainingNoEntriesFails() throws IOException {
File file = new File(this.temp, "empty.jar");
try (FileWriter writer = new FileWriter(file)) {
writer.write("text");
}
given(this.context.getArchiveFile()).willReturn(file);
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
.withMessageContaining("not compatible");
}
@Test
void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws Exception {
this.jarFile = createJarFile("test.jar", (out) -> {
try {
out.putNextEntry(new ZipEntry("e/../../e.jar"));
out.closeEntry();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
});
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
.withMessageContaining("Entry 'e/../../e.jar' would be written");
}
private File createJarFile(String name) throws Exception {
return createJarFile(name, (out) -> {
});
}
private File createJarFile(String name, Consumer<ZipOutputStream> streamHandler) throws Exception {
File file = new File(this.temp, name);
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) {
out.putNextEntry(entry("a/"));
out.closeEntry();
out.putNextEntry(entry("a/a.jar"));
out.closeEntry();
out.putNextEntry(entry("b/"));
out.closeEntry();
out.putNextEntry(entry("b/b.jar"));
out.closeEntry();
out.putNextEntry(entry("c/"));
out.closeEntry();
out.putNextEntry(entry("c/c.jar"));
out.closeEntry();
out.putNextEntry(entry("d/"));
out.closeEntry();
out.putNextEntry(entry("META-INF/MANIFEST.MF"));
out.write(getFile("test-manifest.MF").getBytes());
out.closeEntry();
streamHandler.accept(out);
}
return file;
}
private ZipEntry entry(String path) {
ZipEntry entry = new ZipEntry(path);
entry.setCreationTime(CREATION_TIME);
entry.setLastModifiedTime(LAST_MODIFIED_TIME);
entry.setLastAccessTime(LAST_ACCESS_TIME);
return entry;
}
private String getFile(String fileName) throws Exception {
ClassPathResource resource = new ClassPathResource(fileName, getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
}
private static final class TestLayers implements Layers {
@Override
public Iterator<String> iterator() {
return Arrays.asList("a", "b", "c", "d").iterator();
}
@Override
public String getLayer(String entryName) {
if (entryName.startsWith("a")) {
return "a";
}
if (entryName.startsWith("b")) {
return "b";
}
return "c";
}
@Override
public String getApplicationLayerName() {
return "application";
}
}
}

View File

@@ -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");
}
}

View File

@@ -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<String, String> 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<String, String> getAttributes(Manifest manifest) {
Map<String, String> 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)));
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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.InputStreamReader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
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.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link IndexedLayers}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class IndexedLayersTests {
@TempDir
File temp;
@Test
void createWhenIndexFileIsEmptyThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ", "BOOT-INF/classes"))
.withMessage("Empty layer index file loaded");
}
@Test
void createWhenIndexFileIsMalformedThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test", "BOOT-INF/classes"))
.withMessage("Layer index file is malformed");
}
@Test
void iteratorReturnsLayers() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThat(layers).containsExactly("test", "empty", "application");
}
@Test
void getLayerWhenMatchesNameReturnsLayer() throws Exception {
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(), "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(), "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(), "BOOT-INF/classes");
assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application");
}
@Test
void getShouldReturnIndexedLayersFromContext() throws Exception {
Context context = mock(Context.class);
given(context.getArchiveFile()).willReturn(createWarFile("test.war"));
IndexedLayers layers = IndexedLayers.get(context);
assertThat(layers.getLayer(mockEntry("WEB-INF/lib/a.jar"))).isEqualTo("test");
}
private String getIndex() throws Exception {
return getFile("test-layers.idx");
}
private String getFile(String fileName) throws Exception {
ClassPathResource resource = new ClassPathResource(fileName, getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
}
private ZipEntry mockEntry(String name) {
ZipEntry entry = mock(ZipEntry.class);
given(entry.getName()).willReturn(name);
return entry;
}
private File createWarFile(String name) throws Exception {
File file = new File(this.temp, name);
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) {
out.putNextEntry(new ZipEntry("WEB-INF/lib/a/"));
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/lib/a/a.jar"));
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/classes/Demo.class"));
out.closeEntry();
out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
out.write(getFile("test-war-manifest.MF").getBytes());
out.closeEntry();
out.putNextEntry(new ZipEntry("WEB-INF/layers.idx"));
out.write(getFile("test-war-layers.idx").getBytes());
out.closeEntry();
}
return file;
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.jar.JarEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.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 LayerToolsJarMode}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class LayerToolsJarModeTests {
private static final String[] NO_ARGS = {};
private TestPrintStream out;
private PrintStream systemOut;
@TempDir
File temp;
@BeforeEach
void setup() throws Exception {
Context context = mock(Context.class);
given(context.getArchiveFile()).willReturn(createJarFile("test.jar"));
this.out = new TestPrintStream(this);
this.systemOut = System.out;
System.setOut(this.out);
LayerToolsJarMode.contextOverride = context;
System.setProperty("jarmode", "layertools");
}
@AfterEach
void restore() {
System.setOut(this.systemOut);
LayerToolsJarMode.contextOverride = null;
System.clearProperty("jarmode");
}
@Test
void mainWithNoParametersShowsHelp() {
new LayerToolsJarMode().run("layertools", NO_ARGS);
assertThat(this.out).hasSameContentAsResource("layertools-help-output.txt");
}
@Test
void mainWithArgRunsCommand() {
new LayerToolsJarMode().run("layertools", new String[] { "list" });
assertThat(this.out).hasSameContentAsResource("layertools-list-output.txt");
}
@Test
void mainWithUnknownCommandShowsErrorAndHelp() {
new LayerToolsJarMode().run("layertools", new String[] { "invalid" });
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("layertools-error-option-unknown-output.txt");
}
@Test
void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() {
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" });
assertThat(this.out).hasSameContentAsResource("layertools-error-option-missing-value-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(" - \"0002 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);
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.jar.JarEntry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ListCommand}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
@ExtendWith(MockitoExtension.class)
class ListCommandTests {
@TempDir
File temp;
@Mock
private Context context;
private ListCommand command;
private TestPrintStream out;
@BeforeEach
void setup() throws Exception {
File jarFile = createJarFile("test.jar");
given(this.context.getArchiveFile()).willReturn(jarFile);
this.command = new ListCommand(this.context);
this.out = new TestPrintStream(this);
}
@Test
void listLayersShouldListLayers() {
Layers layers = IndexedLayers.get(this.context);
this.command.printLayers(layers, this.out);
assertThat(this.out).hasSameContentAsResource("list-output-without-deprecation.txt");
}
private File createJarFile(String name) throws Exception {
File file = new File(this.temp, name);
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
writeLayersIndex(jarOutputStream);
String entryPrefix = "BOOT-INF/lib/";
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "b/"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "b/b.jar"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "c/"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "c/c.jar"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "d/"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));
jarOutputStream.write(getFile("test-manifest.MF").getBytes());
jarOutputStream.closeEntry();
}
return file;
}
private void writeLayersIndex(ZipOutputStream out) throws IOException {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
out.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(out, 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();
}
private String getFile(String fileName) throws Exception {
return new ClassPathResource(fileName, getClass()).getContentAsString(StandardCharsets.UTF_8);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Option, String> options, List<String> parameters) {
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.Assertions;
import org.springframework.boot.jarmode.tools.TestPrintStream.PrintStreamAssert;
import org.springframework.util.FileCopyUtils;
/**
* {@link PrintStream} that can be used for testing.
*
* @author Phillip Webb
*/
class TestPrintStream extends PrintStream implements AssertProvider<PrintStreamAssert> {
private final Class<?> testClass;
TestPrintStream(Object testInstance) {
super(new ByteArrayOutputStream());
this.testClass = testInstance.getClass();
}
@Override
public PrintStreamAssert assertThat() {
return new PrintStreamAssert(this);
}
@Override
public String toString() {
return this.out.toString();
}
static final class PrintStreamAssert extends AbstractAssert<PrintStreamAssert, TestPrintStream> {
private PrintStreamAssert(TestPrintStream actual) {
super(actual, PrintStreamAssert.class);
}
void hasSameContentAsResource(String resource) {
try {
InputStream stream = this.actual.testClass.getResourceAsStream(resource);
String content = FileCopyUtils.copyToString(new InputStreamReader(stream, StandardCharsets.UTF_8));
Assertions.assertThat(this.actual).hasToString(content);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,3 @@
- "BOOT-INF/lib/dependency-1.jar"
- "BOOT-INF/lib/dependency-2.jar"
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"

View File

@@ -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/"

View File

@@ -0,0 +1,6 @@
Usage:
java -Djarmode=tools -jar test.jar
Available commands:
test Description of test
help Help about any command

View File

@@ -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

View File

@@ -0,0 +1,10 @@
Error: Unknown command "invalid"
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

View File

@@ -0,0 +1,11 @@
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
Usage:
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
Options:
--destination string The destination to extract files to

View File

@@ -0,0 +1,11 @@
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
Usage:
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
Options:
--destination string The destination to extract files to

View File

@@ -0,0 +1,8 @@
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

View File

@@ -0,0 +1,5 @@
Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead.
0001
0002
0003

View File

@@ -0,0 +1,4 @@
dependencies
spring-boot-loader
snapshot-dependencies
application

View File

@@ -0,0 +1,8 @@
- "test":
- "BOOT-INF/lib/a.jar"
- "BOOT-INF/lib/b.jar"
- "empty":
- "application":
- "BOOT-INF/classes/Demo.class"
- "META-INF/"
- "a b/c d"

View File

@@ -0,0 +1,12 @@
Manifest-Version: 1.0
Created-By: Maven JAR Plugin
Build-Jdk-Spec: 11
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.example.DemoApplication
Spring-Boot-Version: 2.5.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

View File

@@ -0,0 +1,6 @@
- "test":
- "WEB-INF/lib/a.jar"
- "WEB-INF/lib/b.jar"
- "empty":
- "application":
- "WEB-INF/classes/Demo.class"

View File

@@ -0,0 +1,12 @@
Manifest-Version: 1.0
Created-By: Maven WAR Plugin 3.3.1
Build-Jdk-Spec: 11
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.example.DemoApplication
Spring-Boot-Version: 2.5.0-SNAPSHOT
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Spring-Boot-Classpath-Index: WEB-INF/classpath.idx
Spring-Boot-Layers-Index: WEB-INF/layers.idx

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
Help about any command
Usage:
java -Djarmode=tools -jar test.jar help [<command>]

View File

@@ -0,0 +1,4 @@
List layers from the jar that can be extracted
Usage:
java -Djarmode=tools -jar test.jar list-layers

View File

@@ -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

View File

@@ -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