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:
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
spring.application.name=test
|
||||
@@ -0,0 +1,3 @@
|
||||
- "BOOT-INF/lib/dependency-1.jar"
|
||||
- "BOOT-INF/lib/dependency-2.jar"
|
||||
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"
|
||||
@@ -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/"
|
||||
@@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar
|
||||
|
||||
Available commands:
|
||||
test Description of test
|
||||
help Help about any command
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead.
|
||||
|
||||
0001
|
||||
0002
|
||||
0003
|
||||
@@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
dependencies
|
||||
spring-boot-loader
|
||||
snapshot-dependencies
|
||||
application
|
||||
@@ -0,0 +1,3 @@
|
||||
0001
|
||||
0002
|
||||
0003
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
- "test":
|
||||
- "WEB-INF/lib/a.jar"
|
||||
- "WEB-INF/lib/b.jar"
|
||||
- "empty":
|
||||
- "application":
|
||||
- "WEB-INF/classes/Demo.class"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
Help about any command
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar help [<command>]
|
||||
@@ -0,0 +1,4 @@
|
||||
List layers from the jar that can be extracted
|
||||
|
||||
Usage:
|
||||
java -Djarmode=tools -jar test.jar list-layers
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user