Browse Source
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-38276pull/39856/head
121 changed files with 2780 additions and 612 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
plugins { |
||||
id 'java' |
||||
id 'org.springframework.boot' version '{version}' |
||||
} |
||||
|
||||
bootJar { |
||||
mainClass = 'com.example.Application' |
||||
{includeTools} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
plugins { |
||||
id 'java' |
||||
id 'org.springframework.boot' version '{version}' |
||||
id 'war' |
||||
} |
||||
|
||||
bootWar { |
||||
mainClass = 'com.example.Application' |
||||
{includeTools} |
||||
} |
||||
@ -1,119 +0,0 @@
@@ -1,119 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.OutputStream; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.attribute.BasicFileAttributeView; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.zip.ZipEntry; |
||||
import java.util.zip.ZipInputStream; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StreamUtils; |
||||
|
||||
/** |
||||
* The {@code 'extract'} tools command. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ExtractCommand extends Command { |
||||
|
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to"); |
||||
|
||||
private final Context context; |
||||
|
||||
private final Layers layers; |
||||
|
||||
ExtractCommand(Context context) { |
||||
this(context, Layers.get(context)); |
||||
} |
||||
|
||||
ExtractCommand(Context context, Layers layers) { |
||||
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION), |
||||
Parameters.of("[<layer>...]")); |
||||
this.context = context; |
||||
this.layers = layers; |
||||
} |
||||
|
||||
@Override |
||||
protected void run(Map<Option, String> options, List<String> parameters) { |
||||
try { |
||||
File destination = options.containsKey(DESTINATION_OPTION) ? new File(options.get(DESTINATION_OPTION)) |
||||
: this.context.getWorkingDir(); |
||||
for (String layer : this.layers) { |
||||
if (parameters.isEmpty() || parameters.contains(layer)) { |
||||
mkDirs(new File(destination, layer)); |
||||
} |
||||
} |
||||
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) { |
||||
ZipEntry entry = zip.getNextEntry(); |
||||
Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString() |
||||
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled"); |
||||
while (entry != null) { |
||||
if (!entry.isDirectory()) { |
||||
String layer = this.layers.getLayer(entry); |
||||
if (parameters.isEmpty() || parameters.contains(layer)) { |
||||
write(zip, entry, new File(destination, layer)); |
||||
} |
||||
} |
||||
entry = zip.getNextEntry(); |
||||
} |
||||
} |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException { |
||||
String canonicalOutputPath = destination.getCanonicalPath() + File.separator; |
||||
File file = new File(destination, entry.getName()); |
||||
String canonicalEntryPath = file.getCanonicalPath(); |
||||
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), |
||||
() -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath |
||||
+ "'. This is outside the output location of '" + canonicalOutputPath |
||||
+ "'. Verify the contents of your archive."); |
||||
mkParentDirs(file); |
||||
try (OutputStream out = new FileOutputStream(file)) { |
||||
StreamUtils.copy(zip, out); |
||||
} |
||||
try { |
||||
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) |
||||
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); |
||||
} |
||||
catch (IOException ex) { |
||||
// File system does not support setting time attributes. Continue.
|
||||
} |
||||
} |
||||
|
||||
private void mkParentDirs(File file) throws IOException { |
||||
mkDirs(file.getParentFile()); |
||||
} |
||||
|
||||
private void mkDirs(File file) throws IOException { |
||||
if (!file.exists() && !file.mkdirs()) { |
||||
throw new IOException("Unable to create directory " + file); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,116 +0,0 @@
@@ -1,116 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.layertools; |
||||
|
||||
import java.util.ArrayDeque; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.Deque; |
||||
import java.util.List; |
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode; |
||||
|
||||
/** |
||||
* {@link JarMode} providing {@code "layertools"} support. |
||||
* |
||||
* @author Phillip Webb |
||||
* @author Scott Frederick |
||||
* @since 2.3.0 |
||||
*/ |
||||
public class LayerToolsJarMode implements JarMode { |
||||
|
||||
@Override |
||||
public boolean accepts(String mode) { |
||||
return "layertools".equalsIgnoreCase(mode); |
||||
} |
||||
|
||||
@Override |
||||
public void run(String mode, String[] args) { |
||||
try { |
||||
new Runner().run(args); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
static class Runner { |
||||
|
||||
static Context contextOverride; |
||||
|
||||
private final List<Command> commands; |
||||
|
||||
private final HelpCommand help; |
||||
|
||||
Runner() { |
||||
Context context = (contextOverride != null) ? contextOverride : new Context(); |
||||
this.commands = getCommands(context); |
||||
this.help = new HelpCommand(context, this.commands); |
||||
} |
||||
|
||||
private void run(String[] args) { |
||||
run(dequeOf(args)); |
||||
} |
||||
|
||||
private void run(Deque<String> args) { |
||||
if (!args.isEmpty()) { |
||||
String commandName = args.removeFirst(); |
||||
Command command = Command.find(this.commands, commandName); |
||||
if (command != null) { |
||||
runCommand(command, args); |
||||
return; |
||||
} |
||||
printError("Unknown command \"" + commandName + "\""); |
||||
} |
||||
this.help.run(args); |
||||
} |
||||
|
||||
private void runCommand(Command command, Deque<String> args) { |
||||
try { |
||||
command.run(args); |
||||
} |
||||
catch (UnknownOptionException ex) { |
||||
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command"); |
||||
this.help.run(dequeOf(command.getName())); |
||||
} |
||||
catch (MissingValueException ex) { |
||||
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() |
||||
+ " command requires a value"); |
||||
this.help.run(dequeOf(command.getName())); |
||||
} |
||||
} |
||||
|
||||
private void printError(String errorMessage) { |
||||
System.out.println("Error: " + errorMessage); |
||||
System.out.println(); |
||||
} |
||||
|
||||
private Deque<String> dequeOf(String... args) { |
||||
return new ArrayDeque<>(Arrays.asList(args)); |
||||
} |
||||
|
||||
static List<Command> getCommands(Context context) { |
||||
List<Command> commands = new ArrayList<>(); |
||||
commands.add(new ListCommand(context)); |
||||
commands.add(new ExtractCommand(context)); |
||||
return Collections.unmodifiableList(commands); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
# Jar Modes |
||||
org.springframework.boot.loader.jarmode.JarMode=\ |
||||
org.springframework.boot.jarmode.layertools.LayerToolsJarMode |
||||
@ -1,103 +0,0 @@
@@ -1,103 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2023 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileOutputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.OutputStreamWriter; |
||||
import java.io.Writer; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.jar.JarEntry; |
||||
import java.util.zip.ZipOutputStream; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.io.TempDir; |
||||
|
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.util.FileCopyUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link HelpCommand}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class HelpCommandTests { |
||||
|
||||
private HelpCommand command; |
||||
|
||||
private TestPrintStream out; |
||||
|
||||
@TempDir |
||||
File temp; |
||||
|
||||
@BeforeEach |
||||
void setup() throws Exception { |
||||
Context context = mock(Context.class); |
||||
given(context.getArchiveFile()).willReturn(createJarFile("test.jar")); |
||||
this.command = new HelpCommand(context, LayerToolsJarMode.Runner.getCommands(context)); |
||||
this.out = new TestPrintStream(this); |
||||
} |
||||
|
||||
@Test |
||||
void runWhenHasNoParametersPrintsUsage() { |
||||
this.command.run(this.out, Collections.emptyList()); |
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt"); |
||||
} |
||||
|
||||
@Test |
||||
void runWhenHasNoCommandParameterPrintsUsage() { |
||||
this.command.run(this.out, Arrays.asList("extract")); |
||||
System.out.println(this.out); |
||||
assertThat(this.out).hasSameContentAsResource("help-extract-output.txt"); |
||||
} |
||||
|
||||
private File createJarFile(String name) throws Exception { |
||||
File file = new File(this.temp, name); |
||||
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) { |
||||
jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); |
||||
jarOutputStream.write(getFile("test-manifest.MF").getBytes()); |
||||
jarOutputStream.closeEntry(); |
||||
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); |
||||
jarOutputStream.putNextEntry(indexEntry); |
||||
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); |
||||
writer.write("- \"0001\":\n"); |
||||
writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); |
||||
writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); |
||||
writer.write("- \"0002\":\n"); |
||||
writer.write(" - \"BOOT-INF/lib/c.jar\"\n"); |
||||
writer.write("- \"0003\":\n"); |
||||
writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); |
||||
writer.flush(); |
||||
} |
||||
return file; |
||||
} |
||||
|
||||
private String getFile(String fileName) throws Exception { |
||||
ClassPathResource resource = new ClassPathResource(fileName, getClass()); |
||||
InputStreamReader reader = new InputStreamReader(resource.getInputStream()); |
||||
return FileCopyUtils.copyToString(reader); |
||||
} |
||||
|
||||
} |
||||
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
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,436 @@
@@ -0,0 +1,436 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.OutputStream; |
||||
import java.io.PrintStream; |
||||
import java.io.UncheckedIOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.attribute.BasicFileAttributeView; |
||||
import java.nio.file.attribute.FileTime; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.jar.JarEntry; |
||||
import java.util.jar.JarOutputStream; |
||||
import java.util.jar.Manifest; |
||||
import java.util.zip.ZipEntry; |
||||
import java.util.zip.ZipInputStream; |
||||
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry; |
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; |
||||
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StreamUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* The {@code 'extract'} tools command. |
||||
* |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class ExtractCommand extends Command { |
||||
|
||||
/** |
||||
* Option to create a launcher. |
||||
*/ |
||||
static final Option LAUNCHER_OPTION = Option.of("launcher", null, "Whether to extract the Spring Boot launcher"); |
||||
|
||||
/** |
||||
* Option to extract layers. |
||||
*/ |
||||
static final Option LAYERS_OPTION = Option.of("layers", "string list", "Layers to extract", true); |
||||
|
||||
/** |
||||
* Option to specify the destination to write to. |
||||
*/ |
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string", |
||||
"Directory to extract files to. Defaults to the current working directory"); |
||||
|
||||
private static final Option LIBRARIES_DIRECTORY_OPTION = Option.of("libraries", "string", |
||||
"Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/"); |
||||
|
||||
private static final Option RUNNER_FILENAME_OPTION = Option.of("runner-filename", "string", |
||||
"Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar"); |
||||
|
||||
private final Context context; |
||||
|
||||
private final Layers layers; |
||||
|
||||
ExtractCommand(Context context) { |
||||
this(context, null); |
||||
} |
||||
|
||||
ExtractCommand(Context context, Layers layers) { |
||||
super("extract", "Extract the contents from the jar", Options.of(LAUNCHER_OPTION, LAYERS_OPTION, |
||||
DESTINATION_OPTION, LIBRARIES_DIRECTORY_OPTION, RUNNER_FILENAME_OPTION), Parameters.none()); |
||||
this.context = context; |
||||
this.layers = layers; |
||||
} |
||||
|
||||
@Override |
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) { |
||||
try { |
||||
checkJarCompatibility(); |
||||
File destination = getWorkingDirectory(options); |
||||
FileResolver fileResolver = getFileResolver(destination, options); |
||||
fileResolver.createDirectories(); |
||||
if (options.containsKey(LAUNCHER_OPTION)) { |
||||
extractArchive(fileResolver); |
||||
} |
||||
else { |
||||
JarStructure jarStructure = getJarStructure(); |
||||
extractLibraries(fileResolver, jarStructure, options); |
||||
createRunner(jarStructure, fileResolver, options); |
||||
} |
||||
} |
||||
catch (IOException ex) { |
||||
throw new UncheckedIOException(ex); |
||||
} |
||||
catch (LayersNotEnabledException ex) { |
||||
printError(out, "Layers are not enabled"); |
||||
} |
||||
} |
||||
|
||||
private void checkJarCompatibility() throws IOException { |
||||
File file = this.context.getArchiveFile(); |
||||
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) { |
||||
ZipEntry entry = stream.getNextEntry(); |
||||
Assert.state(entry != null, |
||||
() -> "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled" |
||||
.formatted(file)); |
||||
} |
||||
} |
||||
|
||||
private void printError(PrintStream out, String message) { |
||||
out.println("Error: " + message); |
||||
out.println(); |
||||
} |
||||
|
||||
private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map<Option, String> options) |
||||
throws IOException { |
||||
String librariesDirectory = getLibrariesDirectory(options); |
||||
extractArchive(fileResolver, (zipEntry) -> { |
||||
Entry entry = jarStructure.resolve(zipEntry); |
||||
if (isType(entry, Type.LIBRARY)) { |
||||
return librariesDirectory + entry.location(); |
||||
} |
||||
return null; |
||||
}); |
||||
} |
||||
|
||||
private static String getLibrariesDirectory(Map<Option, String> options) { |
||||
if (options.containsKey(LIBRARIES_DIRECTORY_OPTION)) { |
||||
String value = options.get(LIBRARIES_DIRECTORY_OPTION); |
||||
if (value.endsWith("/")) { |
||||
return value; |
||||
} |
||||
return value + "/"; |
||||
} |
||||
return "lib/"; |
||||
} |
||||
|
||||
private FileResolver getFileResolver(File destination, Map<Option, String> options) { |
||||
String runnerFilename = getRunnerFilename(options); |
||||
if (!options.containsKey(LAYERS_OPTION)) { |
||||
return new NoLayersFileResolver(destination, runnerFilename); |
||||
} |
||||
Layers layers = getLayers(); |
||||
Set<String> layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); |
||||
return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename); |
||||
} |
||||
|
||||
private File getWorkingDirectory(Map<Option, String> options) { |
||||
if (options.containsKey(DESTINATION_OPTION)) { |
||||
return new File(options.get(DESTINATION_OPTION)); |
||||
} |
||||
return this.context.getWorkingDir(); |
||||
} |
||||
|
||||
private JarStructure getJarStructure() { |
||||
IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context.getArchiveFile()); |
||||
Assert.state(jarStructure != null, "Couldn't read classpath index"); |
||||
return jarStructure; |
||||
} |
||||
|
||||
private void extractArchive(FileResolver fileResolver) throws IOException { |
||||
extractArchive(fileResolver, ZipEntry::getName); |
||||
} |
||||
|
||||
private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer) |
||||
throws IOException { |
||||
withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> { |
||||
if (zipEntry.isDirectory()) { |
||||
return; |
||||
} |
||||
String name = entryNameTransformer.getName(zipEntry); |
||||
if (name == null) { |
||||
return; |
||||
} |
||||
File file = fileResolver.resolve(zipEntry, name); |
||||
if (file != null) { |
||||
extractEntry(stream, zipEntry, file); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private Layers getLayers() { |
||||
if (this.layers != null) { |
||||
return this.layers; |
||||
} |
||||
return Layers.get(this.context); |
||||
} |
||||
|
||||
private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map<Option, String> options) |
||||
throws IOException { |
||||
File file = fileResolver.resolveRunner(); |
||||
if (file == null) { |
||||
return; |
||||
} |
||||
String librariesDirectory = getLibrariesDirectory(options); |
||||
Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library); |
||||
mkDirs(file.getParentFile()); |
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) { |
||||
withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> { |
||||
Entry entry = jarStructure.resolve(zipEntry); |
||||
if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) { |
||||
JarEntry jarEntry = createJarEntry(entry.location(), zipEntry); |
||||
output.putNextEntry(jarEntry); |
||||
StreamUtils.copy(stream, output); |
||||
output.closeEntry(); |
||||
} |
||||
})); |
||||
} |
||||
} |
||||
|
||||
private String getRunnerFilename(Map<Option, String> options) { |
||||
if (options.containsKey(RUNNER_FILENAME_OPTION)) { |
||||
return options.get(RUNNER_FILENAME_OPTION); |
||||
} |
||||
return "runner.jar"; |
||||
} |
||||
|
||||
private static boolean isType(Entry entry, Type type) { |
||||
if (entry == null) { |
||||
return false; |
||||
} |
||||
return entry.type() == type; |
||||
} |
||||
|
||||
private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException { |
||||
mkDirs(file.getParentFile()); |
||||
try (OutputStream out = new FileOutputStream(file)) { |
||||
StreamUtils.copy(zip, out); |
||||
} |
||||
try { |
||||
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) |
||||
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); |
||||
} |
||||
catch (IOException ex) { |
||||
// File system does not support setting time attributes. Continue.
|
||||
} |
||||
} |
||||
|
||||
private static void mkDirs(File file) throws IOException { |
||||
if (!file.exists() && !file.mkdirs()) { |
||||
throw new IOException("Unable to create directory " + file); |
||||
} |
||||
} |
||||
|
||||
private static JarEntry createJarEntry(String location, ZipEntry originalEntry) { |
||||
JarEntry jarEntry = new JarEntry(location); |
||||
FileTime lastModifiedTime = originalEntry.getLastModifiedTime(); |
||||
if (lastModifiedTime != null) { |
||||
jarEntry.setLastModifiedTime(lastModifiedTime); |
||||
} |
||||
FileTime lastAccessTime = originalEntry.getLastAccessTime(); |
||||
if (lastAccessTime != null) { |
||||
jarEntry.setLastAccessTime(lastAccessTime); |
||||
} |
||||
FileTime creationTime = originalEntry.getCreationTime(); |
||||
if (creationTime != null) { |
||||
jarEntry.setCreationTime(creationTime); |
||||
} |
||||
return jarEntry; |
||||
} |
||||
|
||||
private static void withZipEntries(File file, ThrowingConsumer callback) throws IOException { |
||||
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) { |
||||
ZipEntry entry = stream.getNextEntry(); |
||||
while (entry != null) { |
||||
if (StringUtils.hasLength(entry.getName())) { |
||||
callback.accept(stream, entry); |
||||
} |
||||
entry = stream.getNextEntry(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException { |
||||
String canonicalOutputPath = directory.getCanonicalPath() + File.separator; |
||||
String canonicalEntryPath = file.getCanonicalPath(); |
||||
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), |
||||
() -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." |
||||
.formatted(name, canonicalEntryPath, canonicalOutputPath)); |
||||
return file; |
||||
} |
||||
|
||||
@FunctionalInterface |
||||
private interface EntryNameTransformer { |
||||
|
||||
String getName(ZipEntry entry); |
||||
|
||||
} |
||||
|
||||
@FunctionalInterface |
||||
private interface ThrowingConsumer { |
||||
|
||||
void accept(ZipInputStream stream, ZipEntry entry) throws IOException; |
||||
|
||||
} |
||||
|
||||
private interface FileResolver { |
||||
|
||||
/** |
||||
* Creates needed directories. |
||||
* @throws IOException if something went wrong |
||||
*/ |
||||
void createDirectories() throws IOException; |
||||
|
||||
/** |
||||
* Resolves the given {@link ZipEntry} to a file. |
||||
* @param entry the zip entry |
||||
* @param newName the new name of the file |
||||
* @return file where the contents should be written or {@code null} if this entry |
||||
* should be skipped |
||||
* @throws IOException if something went wrong |
||||
*/ |
||||
default File resolve(ZipEntry entry, String newName) throws IOException { |
||||
return resolve(entry.getName(), newName); |
||||
} |
||||
|
||||
/** |
||||
* Resolves the given name to a file. |
||||
* @param originalName the original name of the file |
||||
* @param newName the new name of the file |
||||
* @return file where the contents should be written or {@code null} if this name |
||||
* should be skipped |
||||
* @throws IOException if something went wrong |
||||
*/ |
||||
File resolve(String originalName, String newName) throws IOException; |
||||
|
||||
/** |
||||
* Resolves the file for the runner. |
||||
* @return the file for the runner or {@code null} if the runner should be skipped |
||||
* @throws IOException if something went wrong |
||||
*/ |
||||
File resolveRunner() throws IOException; |
||||
|
||||
} |
||||
|
||||
private static final class NoLayersFileResolver implements FileResolver { |
||||
|
||||
private final File directory; |
||||
|
||||
private final String runnerFilename; |
||||
|
||||
private NoLayersFileResolver(File directory, String runnerFilename) { |
||||
this.directory = directory; |
||||
this.runnerFilename = runnerFilename; |
||||
} |
||||
|
||||
@Override |
||||
public void createDirectories() { |
||||
} |
||||
|
||||
@Override |
||||
public File resolve(String originalName, String newName) throws IOException { |
||||
return assertFileIsContainedInDirectory(this.directory, new File(this.directory, newName), newName); |
||||
} |
||||
|
||||
@Override |
||||
public File resolveRunner() throws IOException { |
||||
return resolve(this.runnerFilename, this.runnerFilename); |
||||
} |
||||
|
||||
} |
||||
|
||||
private static final class LayersFileResolver implements FileResolver { |
||||
|
||||
private final Layers layers; |
||||
|
||||
private final Set<String> layersToExtract; |
||||
|
||||
private final File directory; |
||||
|
||||
private final String runnerFilename; |
||||
|
||||
LayersFileResolver(File directory, Layers layers, Set<String> layersToExtract, String runnerFilename) { |
||||
this.layers = layers; |
||||
this.layersToExtract = layersToExtract; |
||||
this.directory = directory; |
||||
this.runnerFilename = runnerFilename; |
||||
} |
||||
|
||||
@Override |
||||
public void createDirectories() throws IOException { |
||||
for (String layer : this.layers) { |
||||
if (shouldExtractLayer(layer)) { |
||||
mkDirs(getLayerDirectory(layer)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public File resolve(String originalName, String newName) throws IOException { |
||||
String layer = this.layers.getLayer(originalName); |
||||
if (shouldExtractLayer(layer)) { |
||||
File directory = getLayerDirectory(layer); |
||||
return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public File resolveRunner() throws IOException { |
||||
String layer = this.layers.getApplicationLayerName(); |
||||
if (shouldExtractLayer(layer)) { |
||||
File directory = getLayerDirectory(layer); |
||||
return assertFileIsContainedInDirectory(directory, new File(directory, this.runnerFilename), |
||||
this.runnerFilename); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private File getLayerDirectory(String layer) { |
||||
return new File(this.directory, layer); |
||||
} |
||||
|
||||
private boolean shouldExtractLayer(String layer) { |
||||
if (this.layersToExtract.isEmpty()) { |
||||
return true; |
||||
} |
||||
return this.layersToExtract.contains(layer); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* The {@code 'extract'} tools command. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ExtractLayersCommand extends Command { |
||||
|
||||
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to"); |
||||
|
||||
private final ExtractCommand delegate; |
||||
|
||||
ExtractLayersCommand(Context context) { |
||||
this(context, null); |
||||
} |
||||
|
||||
ExtractLayersCommand(Context context, Layers layers) { |
||||
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION), |
||||
Parameters.of("[<layer>...]")); |
||||
this.delegate = new ExtractCommand(context, layers); |
||||
} |
||||
|
||||
@Override |
||||
boolean isDeprecated() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
String getDeprecationMessage() { |
||||
return "Use '-Djarmode=tools extract --layers --launcher' instead."; |
||||
} |
||||
|
||||
@Override |
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) { |
||||
Map<Option, String> rewrittenOptions = new HashMap<>(); |
||||
if (options.containsKey(DESTINATION_OPTION)) { |
||||
rewrittenOptions.put(ExtractCommand.DESTINATION_OPTION, options.get(DESTINATION_OPTION)); |
||||
} |
||||
rewrittenOptions.put(ExtractCommand.LAYERS_OPTION, StringUtils.collectionToCommaDelimitedString(parameters)); |
||||
rewrittenOptions.put(ExtractCommand.LAUNCHER_OPTION, null); |
||||
this.delegate.run(out, rewrittenOptions, Collections.emptyList()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.nio.file.NoSuchFileException; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.function.UnaryOperator; |
||||
import java.util.jar.Attributes; |
||||
import java.util.jar.Attributes.Name; |
||||
import java.util.jar.JarFile; |
||||
import java.util.jar.Manifest; |
||||
import java.util.stream.Collectors; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StreamUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* {@link JarStructure} implementation backed by a {@code classpath.idx} file. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class IndexedJarStructure implements JarStructure { |
||||
|
||||
private static final List<String> MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes", |
||||
"Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index"); |
||||
|
||||
private final Manifest originalManifest; |
||||
|
||||
private final String libLocation; |
||||
|
||||
private final String classesLocation; |
||||
|
||||
private final List<String> classpathEntries; |
||||
|
||||
IndexedJarStructure(Manifest originalManifest, String indexFile) { |
||||
this.originalManifest = originalManifest; |
||||
this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib"); |
||||
this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes"); |
||||
this.classpathEntries = readIndexFile(indexFile); |
||||
} |
||||
|
||||
private static String getLocation(Manifest manifest, String attribute) { |
||||
String location = getMandatoryAttribute(manifest, attribute); |
||||
if (!location.endsWith("/")) { |
||||
location = location + "/"; |
||||
} |
||||
return location; |
||||
} |
||||
|
||||
private static List<String> readIndexFile(String indexFile) { |
||||
String[] lines = Arrays.stream(indexFile.split("\n")) |
||||
.map((line) -> line.replace("\r", "")) |
||||
.filter(StringUtils::hasText) |
||||
.toArray(String[]::new); |
||||
List<String> classpathEntries = new ArrayList<>(); |
||||
for (String line : lines) { |
||||
if (line.startsWith("- ")) { |
||||
classpathEntries.add(line.substring(3, line.length() - 1)); |
||||
} |
||||
else { |
||||
throw new IllegalStateException("Classpath index file is malformed"); |
||||
} |
||||
} |
||||
Assert.state(!classpathEntries.isEmpty(), "Empty classpath index file loaded"); |
||||
return classpathEntries; |
||||
} |
||||
|
||||
@Override |
||||
public String getClassesLocation() { |
||||
return this.classesLocation; |
||||
} |
||||
|
||||
@Override |
||||
public Entry resolve(String name) { |
||||
if (this.classpathEntries.contains(name)) { |
||||
return new Entry(name, toStructureDependency(name), Type.LIBRARY); |
||||
} |
||||
else if (name.startsWith(this.classesLocation)) { |
||||
return new Entry(name, name.substring(this.classesLocation.length()), Type.APPLICATION_CLASS_OR_RESOURCE); |
||||
} |
||||
else if (name.startsWith("org/springframework/boot/loader")) { |
||||
return new Entry(name, name, Type.LOADER); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer) { |
||||
Manifest manifest = new Manifest(this.originalManifest); |
||||
Attributes attributes = manifest.getMainAttributes(); |
||||
for (String denied : MANIFEST_DENY_LIST) { |
||||
attributes.remove(new Name(denied)); |
||||
} |
||||
attributes.put(Name.MAIN_CLASS, getMandatoryAttribute(this.originalManifest, "Start-Class")); |
||||
attributes.put(Name.CLASS_PATH, |
||||
this.classpathEntries.stream() |
||||
.map(this::toStructureDependency) |
||||
.map(libraryTransformer) |
||||
.collect(Collectors.joining(" "))); |
||||
return manifest; |
||||
} |
||||
|
||||
private String toStructureDependency(String libEntryName) { |
||||
Assert.state(libEntryName.startsWith(this.libLocation), "Invalid library location " + libEntryName); |
||||
return libEntryName.substring(this.libLocation.length()); |
||||
} |
||||
|
||||
private static String getMandatoryAttribute(Manifest manifest, String attribute) { |
||||
String value = manifest.getMainAttributes().getValue(attribute); |
||||
Assert.state(value != null, "Manifest attribute '" + attribute + "' is mandatory"); |
||||
return value; |
||||
} |
||||
|
||||
static IndexedJarStructure get(File file) { |
||||
try { |
||||
try (JarFile jarFile = new JarFile(file)) { |
||||
Manifest manifest = jarFile.getManifest(); |
||||
String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index"); |
||||
ZipEntry entry = jarFile.getEntry(location); |
||||
if (entry != null) { |
||||
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); |
||||
return new IndexedJarStructure(manifest, indexFile); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
catch (FileNotFoundException | NoSuchFileException ex) { |
||||
return null; |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.util.function.UnaryOperator; |
||||
import java.util.jar.Manifest; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
/** |
||||
* Provide information about a fat jar structure that is meant to be extracted. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
interface JarStructure { |
||||
|
||||
/** |
||||
* Resolve the specified {@link ZipEntry}, return {@code null} if the entry should not |
||||
* be handled. |
||||
* @param entry the entry to handle |
||||
* @return the resolved {@link Entry} |
||||
*/ |
||||
default Entry resolve(ZipEntry entry) { |
||||
return resolve(entry.getName()); |
||||
} |
||||
|
||||
/** |
||||
* Resolve the entry with the specified name, return {@code null} if the entry should |
||||
* not be handled. |
||||
* @param name the name of the entry to handle |
||||
* @return the resolved {@link Entry} |
||||
*/ |
||||
Entry resolve(String name); |
||||
|
||||
/** |
||||
* Create the {@link Manifest} for the launcher jar, applying the specified operator |
||||
* on each classpath entry. |
||||
* @param libraryTransformer the operator to apply on each classpath entry |
||||
* @return the manifest to use for the launcher jar |
||||
*/ |
||||
Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer); |
||||
|
||||
/** |
||||
* Return the location of the application classes. |
||||
* @return the location of the application classes |
||||
*/ |
||||
String getClassesLocation(); |
||||
|
||||
/** |
||||
* An entry to handle in the exploded structure. |
||||
* |
||||
* @param originalLocation the original location |
||||
* @param location the relative location |
||||
* @param type of the entry |
||||
*/ |
||||
record Entry(String originalLocation, String location, Type type) { |
||||
enum Type { |
||||
|
||||
LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER |
||||
|
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode; |
||||
|
||||
/** |
||||
* {@link JarMode} providing {@code "layertools"} support. |
||||
* |
||||
* @author Phillip Webb |
||||
* @author Scott Frederick |
||||
* @since 2.3.0 |
||||
*/ |
||||
public class LayerToolsJarMode implements JarMode { |
||||
|
||||
static Context contextOverride; |
||||
|
||||
@Override |
||||
public boolean accepts(String mode) { |
||||
return "layertools".equalsIgnoreCase(mode); |
||||
} |
||||
|
||||
@Override |
||||
public void run(String mode, String[] args) { |
||||
try { |
||||
Context context = (contextOverride != null) ? contextOverride : new Context(); |
||||
new Runner(System.out, context, getCommands(context)).run(args); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
static List<Command> getCommands(Context context) { |
||||
return List.of(new ListCommand(context), new ExtractLayersCommand(context)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; |
||||
|
||||
/** |
||||
* The {@code 'list-layers'} tools command. |
||||
* |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class ListLayersCommand extends Command { |
||||
|
||||
private final Context context; |
||||
|
||||
ListLayersCommand(Context context) { |
||||
super("list-layers", "List layers from the jar that can be extracted", Options.none(), Parameters.none()); |
||||
this.context = context; |
||||
} |
||||
|
||||
@Override |
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) { |
||||
try { |
||||
Layers layers = Layers.get(this.context); |
||||
printLayers(out, layers); |
||||
} |
||||
catch (LayersNotEnabledException ex) { |
||||
printError(out, "Layers are not enabled"); |
||||
} |
||||
} |
||||
|
||||
void printLayers(PrintStream out, Layers layers) { |
||||
layers.forEach(out::println); |
||||
} |
||||
|
||||
private void printError(PrintStream out, String message) { |
||||
out.println("Error: " + message); |
||||
out.println(); |
||||
} |
||||
|
||||
} |
||||
4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java
4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.ArrayDeque; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Deque; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Runs commands. |
||||
* |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class Runner { |
||||
|
||||
private final PrintStream out; |
||||
|
||||
private final List<Command> commands = new ArrayList<>(); |
||||
|
||||
private final HelpCommand help; |
||||
|
||||
Runner(PrintStream out, Context context, List<Command> commands) { |
||||
this.out = out; |
||||
this.commands.addAll(commands); |
||||
this.help = new HelpCommand(context, commands); |
||||
this.commands.add(this.help); |
||||
} |
||||
|
||||
void run(String... args) { |
||||
run(dequeOf(args)); |
||||
} |
||||
|
||||
private void run(Deque<String> args) { |
||||
if (!args.isEmpty()) { |
||||
String commandName = args.removeFirst(); |
||||
Command command = Command.find(this.commands, commandName); |
||||
if (command != null) { |
||||
runCommand(command, args); |
||||
return; |
||||
} |
||||
printError("Unknown command \"" + commandName + "\""); |
||||
} |
||||
this.help.run(this.out, args); |
||||
} |
||||
|
||||
private void runCommand(Command command, Deque<String> args) { |
||||
if (command.isDeprecated()) { |
||||
printWarning("This command is deprecated. " + command.getDeprecationMessage()); |
||||
} |
||||
try { |
||||
command.run(this.out, args); |
||||
} |
||||
catch (UnknownOptionException ex) { |
||||
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command"); |
||||
this.help.printCommandHelp(this.out, command, false); |
||||
} |
||||
catch (MissingValueException ex) { |
||||
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() + " command requires a value"); |
||||
this.help.printCommandHelp(this.out, command, false); |
||||
} |
||||
} |
||||
|
||||
private void printWarning(String message) { |
||||
this.out.println("Warning: " + message); |
||||
this.out.println(); |
||||
} |
||||
|
||||
private void printError(String message) { |
||||
this.out.println("Error: " + message); |
||||
this.out.println(); |
||||
} |
||||
|
||||
private Deque<String> dequeOf(String... args) { |
||||
return new ArrayDeque<>(Arrays.asList(args)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
/* |
||||
* Copyright 2012-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.jarmode.tools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.List; |
||||
|
||||
import org.springframework.boot.loader.jarmode.JarMode; |
||||
|
||||
/** |
||||
* {@link JarMode} providing {@code "tools"} support. |
||||
* |
||||
* @author Moritz Halbritter |
||||
* @since 3.3.0 |
||||
*/ |
||||
public class ToolsJarMode implements JarMode { |
||||
|
||||
private final Context context; |
||||
|
||||
private final PrintStream out; |
||||
|
||||
public ToolsJarMode() { |
||||
this(null, null); |
||||
} |
||||
|
||||
public ToolsJarMode(Context context, PrintStream out) { |
||||
this.context = (context != null) ? context : new Context(); |
||||
this.out = (out != null) ? out : System.out; |
||||
} |
||||
|
||||
@Override |
||||
public boolean accepts(String mode) { |
||||
return "tools".equalsIgnoreCase(mode); |
||||
} |
||||
|
||||
@Override |
||||
public void run(String mode, String[] args) { |
||||
try { |
||||
new Runner(this.out, this.context, getCommands(this.context)).run(args); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
static List<Command> getCommands(Context context) { |
||||
return List.of(new ExtractCommand(context), new ListLayersCommand(context)); |
||||
} |
||||
|
||||
} |
||||
4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java
4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
# Jar Modes |
||||
org.springframework.boot.loader.jarmode.JarMode=\ |
||||
org.springframework.boot.jarmode.tools.LayerToolsJarMode,\ |
||||
org.springframework.boot.jarmode.tools.ToolsJarMode |
||||
@ -0,0 +1,154 @@
@@ -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,303 @@
@@ -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"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
35
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java
35
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java
@ -0,0 +1,66 @@
@@ -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 @@
@@ -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))); |
||||
} |
||||
|
||||
} |
||||
20
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java
20
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java
@ -0,0 +1,51 @@
@@ -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 @@
@@ -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,102 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
spring.application.name=test |
||||
@ -0,0 +1,3 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled |
||||
|
||||
@ -0,0 +1,6 @@
@@ -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 @@
@@ -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 |
||||
3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt
3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt
2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt
2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt
@ -1,3 +1,5 @@
@@ -1,3 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. |
||||
|
||||
Error: Option "--destination" for the extract command requires a value |
||||
|
||||
Extracts layers from the jar for image creation |
||||
2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt
2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt
@ -1,3 +1,5 @@
@@ -1,3 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead. |
||||
|
||||
Error: Unknown option "--invalid" for the extract command |
||||
|
||||
Extracts layers from the jar for image creation |
||||
3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt
3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead. |
||||
|
||||
0001 |
||||
0002 |
||||
0003 |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
Error: Layers are not enabled |
||||
|
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
dependencies |
||||
spring-boot-loader |
||||
snapshot-dependencies |
||||
application |
||||
0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt
0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
Help about any command |
||||
|
||||
Usage: |
||||
java -Djarmode=tools -jar test.jar help [<command>] |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
List layers from the jar that can be extracted |
||||
|
||||
Usage: |
||||
java -Djarmode=tools -jar test.jar list-layers |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue