Browse Source
Add a new `spring-boot-layertools` module which provides jarmode support for working with layers. The module works with both classic fat jars, as well as layered jars. Closes gh-19849pull/19850/head
27 changed files with 1917 additions and 1 deletions
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
plugins { |
||||
id 'java-library' |
||||
id 'org.springframework.boot.conventions' |
||||
id 'org.springframework.boot.deployed' |
||||
id 'org.springframework.boot.internal-dependency-management' |
||||
} |
||||
|
||||
description = 'Spring Boot Layers Tools' |
||||
|
||||
dependencies { |
||||
api platform(project(':spring-boot-project:spring-boot-parent')) |
||||
|
||||
implementation project(':spring-boot-project:spring-boot-tools:spring-boot-loader') |
||||
implementation project(':spring-boot-project:spring-boot') |
||||
implementation 'org.springframework:spring-core' |
||||
|
||||
testImplementation "org.assertj:assertj-core" |
||||
testImplementation "org.junit.jupiter:junit-jupiter" |
||||
testImplementation "org.mockito:mockito-core" |
||||
} |
||||
|
||||
@ -0,0 +1,335 @@
@@ -0,0 +1,335 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Deque; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.stream.Stream; |
||||
|
||||
/** |
||||
* A command that can be launched from the layertools jarmode. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
abstract class Command { |
||||
|
||||
private final String name; |
||||
|
||||
private final String description; |
||||
|
||||
private final Options options; |
||||
|
||||
private final Parameters parameters; |
||||
|
||||
/** |
||||
* Create a new {@link Command} instance. |
||||
* @param name the name of the command |
||||
* @param description a description of the command |
||||
* @param options the command options |
||||
* @param parameters the command parameters |
||||
*/ |
||||
Command(String name, String description, Options options, Parameters parameters) { |
||||
this.name = name; |
||||
this.description = description; |
||||
this.options = options; |
||||
this.parameters = parameters; |
||||
} |
||||
|
||||
/** |
||||
* Return the name of this command. |
||||
* @return the command name |
||||
*/ |
||||
String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
/** |
||||
* Return the description of this command. |
||||
* @return the command description |
||||
*/ |
||||
String getDescription() { |
||||
return this.description; |
||||
} |
||||
|
||||
/** |
||||
* Return options that this command accepts. |
||||
* @return the command options |
||||
*/ |
||||
Options getOptions() { |
||||
return this.options; |
||||
} |
||||
|
||||
/** |
||||
* Return parameters that this command accepts. |
||||
* @return the command parameters |
||||
*/ |
||||
Parameters getParameters() { |
||||
return this.parameters; |
||||
} |
||||
|
||||
/** |
||||
* Run the command by processing the remaining arguments. |
||||
* @param args a mutable deque of the remaining arguments |
||||
*/ |
||||
final void run(Deque<String> args) { |
||||
List<String> parameters = new ArrayList<>(); |
||||
Map<Option, String> options = new HashMap<>(); |
||||
while (!args.isEmpty()) { |
||||
String arg = args.removeFirst(); |
||||
Option option = this.options.find(arg); |
||||
if (option != null) { |
||||
options.put(option, option.claimArg(args)); |
||||
} |
||||
else { |
||||
parameters.add(arg); |
||||
} |
||||
} |
||||
run(options, parameters); |
||||
} |
||||
|
||||
/** |
||||
* Run the actual command. |
||||
* @param options any options extracted from the arguments |
||||
* @param parameters any parameters extracted from the arguements |
||||
*/ |
||||
protected abstract void run(Map<Option, String> options, List<String> parameters); |
||||
|
||||
/** |
||||
* Static method that can be used to find a single command from a collection. |
||||
* @param commands the commands to search |
||||
* @param name the name of the command to find |
||||
* @return a {@link Command} instance or {@code null}. |
||||
*/ |
||||
static Command find(Collection<? extends Command> commands, String name) { |
||||
for (Command command : commands) { |
||||
if (command.getName().equals(name)) { |
||||
return command; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Parameters that the command accepts. |
||||
*/ |
||||
protected static final class Parameters { |
||||
|
||||
private final List<String> descriptions; |
||||
|
||||
private Parameters(String[] descriptions) { |
||||
this.descriptions = Collections.unmodifiableList(Arrays.asList(descriptions)); |
||||
} |
||||
|
||||
/** |
||||
* Return the parameter descriptions. |
||||
* @return the descriptions |
||||
*/ |
||||
List<String> getDescriptions() { |
||||
return this.descriptions; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.descriptions.toString(); |
||||
} |
||||
|
||||
/** |
||||
* Factory method used if there are no expected parameters. |
||||
* @return a new {@link Parameters} instance |
||||
*/ |
||||
protected static Parameters none() { |
||||
return of(); |
||||
} |
||||
|
||||
/** |
||||
* Factory method used to create a new {@link Parameters} instance with specific |
||||
* descriptions. |
||||
* @param descriptions the parameter descriptions |
||||
* @return a new {@link Parameters} instance with the given descriptions |
||||
*/ |
||||
protected static Parameters of(String... descriptions) { |
||||
return new Parameters(descriptions); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Options that the command accepts. |
||||
*/ |
||||
protected static final class Options { |
||||
|
||||
private final Option[] values; |
||||
|
||||
private Options(Option[] values) { |
||||
this.values = values; |
||||
} |
||||
|
||||
private Option find(String arg) { |
||||
if (arg.startsWith("--")) { |
||||
String name = arg.substring(2); |
||||
for (Option candidate : this.values) { |
||||
if (candidate.getName().equals(name)) { |
||||
return candidate; |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Return if this options collection is empty. |
||||
* @return if there are no options |
||||
*/ |
||||
boolean isEmpty() { |
||||
return this.values.length == 0; |
||||
} |
||||
|
||||
/** |
||||
* Return a stream of each option. |
||||
* @return a stream of the options |
||||
*/ |
||||
Stream<Option> stream() { |
||||
return Arrays.stream(this.values); |
||||
} |
||||
|
||||
/** |
||||
* Factory method used if there are no expected options. |
||||
* @return a new {@link Options} instance |
||||
*/ |
||||
protected static Options none() { |
||||
return of(); |
||||
} |
||||
|
||||
/** |
||||
* Factory method used to create a new {@link Options} instance with specific |
||||
* values. |
||||
* @param values the option values |
||||
* @return a new {@link Options} instance with the given values |
||||
*/ |
||||
protected static Options of(Option... values) { |
||||
return new Options(values); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* An individual option that the command can accepts. Can either be an option with a |
||||
* value (e.g. {@literal --log debug}) or a flag (e.g. {@literal |
||||
* --verbose}). |
||||
*/ |
||||
protected static final class Option { |
||||
|
||||
private final String name; |
||||
|
||||
private final String valueDescription; |
||||
|
||||
private final String description; |
||||
|
||||
private Option(String name, String valueDescription, String description) { |
||||
this.name = name; |
||||
this.description = description; |
||||
this.valueDescription = valueDescription; |
||||
} |
||||
|
||||
/** |
||||
* Return the name of the option. |
||||
* @return the options name |
||||
*/ |
||||
String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
/** |
||||
* Return the description of the expected argument value or {@code null} if this |
||||
* option is a flag/switch. |
||||
* @return the option value description |
||||
*/ |
||||
String getValueDescription() { |
||||
return this.valueDescription; |
||||
} |
||||
|
||||
/** |
||||
* Return the name and the value description combined. |
||||
* @return the name and value description |
||||
*/ |
||||
String getNameAndValueDescription() { |
||||
return this.name + ((this.valueDescription != null) ? " " + this.valueDescription : ""); |
||||
} |
||||
|
||||
/** |
||||
* Return a description of the option. |
||||
* @return the option description |
||||
*/ |
||||
String getDescription() { |
||||
return this.description; |
||||
} |
||||
|
||||
private String claimArg(Deque<String> args) { |
||||
return (this.valueDescription != null) ? args.removeFirst() : null; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (obj == null || getClass() != obj.getClass()) { |
||||
return false; |
||||
} |
||||
return this.name.equals(((Option) obj).name); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return this.name.hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.name; |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create a flag/switch option. |
||||
* @param name the name of the option |
||||
* @param description a description of the option |
||||
* @return a new {@link Option} instance |
||||
*/ |
||||
protected static Option flag(String name, String description) { |
||||
return new Option(name, null, description); |
||||
} |
||||
|
||||
/** |
||||
* Factory method to create value option. |
||||
* @param name the name of the option |
||||
* @param valueDescription a description of the expected value |
||||
* @param description a description of the option |
||||
* @return a new {@link Option} instance |
||||
*/ |
||||
protected static Option of(String name, String valueDescription, String description) { |
||||
return new Option(name, valueDescription, description); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.nio.file.Paths; |
||||
|
||||
import org.springframework.boot.system.ApplicationHome; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Context for use by commands. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class Context { |
||||
|
||||
private final File jarFile; |
||||
|
||||
private final File workingDir; |
||||
|
||||
private String relativeDir; |
||||
|
||||
/** |
||||
* Create a new {@link Context} instance. |
||||
*/ |
||||
Context() { |
||||
this(new ApplicationHome().getSource(), Paths.get(".").toAbsolutePath().normalize().toFile()); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link Context} instance with the specified value. |
||||
* @param jarFile the source jar file |
||||
* @param workingDir the working directory |
||||
*/ |
||||
Context(File jarFile, File workingDir) { |
||||
Assert.state(jarFile != null && jarFile.isFile() && jarFile.exists() |
||||
&& jarFile.getName().toLowerCase().endsWith(".jar"), "Unable to find source JAR"); |
||||
this.jarFile = jarFile; |
||||
this.workingDir = workingDir; |
||||
this.relativeDir = deduceRelativeDir(jarFile.getParentFile(), this.workingDir); |
||||
} |
||||
|
||||
private String deduceRelativeDir(File sourceFolder, File workingDir) { |
||||
String sourcePath = sourceFolder.getAbsolutePath(); |
||||
String workingPath = workingDir.getAbsolutePath(); |
||||
if (sourcePath.equals(workingPath) || !sourcePath.startsWith(workingPath)) { |
||||
return null; |
||||
} |
||||
String relativePath = sourcePath.substring(workingPath.length() + 1); |
||||
return (relativePath.length() > 0) ? relativePath : null; |
||||
} |
||||
|
||||
/** |
||||
* Return the source jar file that is running in tools mode. |
||||
* @return the jar file |
||||
*/ |
||||
File getJarFile() { |
||||
return this.jarFile; |
||||
} |
||||
|
||||
/** |
||||
* Return the current working directory. |
||||
* @return the working dir |
||||
*/ |
||||
File getWorkingDir() { |
||||
return this.workingDir; |
||||
} |
||||
|
||||
/** |
||||
* Return the directory relative to {@link #getWorkingDir()} that contains the jar or |
||||
* {@code null} if none relative directory can be deduced. |
||||
* @return the relative dir ending in {@code /} or {@code null} |
||||
*/ |
||||
String getRelativeJarDir() { |
||||
return this.relativeDir; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
|
||||
/* |
||||
* 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.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.util.List; |
||||
import java.util.Map; |
||||
import java.util.zip.ZipEntry; |
||||
import java.util.zip.ZipInputStream; |
||||
|
||||
import org.springframework.util.StreamUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* 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.getJarFile()))) { |
||||
ZipEntry entry = zip.getNextEntry(); |
||||
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 path = StringUtils.cleanPath(entry.getName()); |
||||
File file = new File(destination, path); |
||||
if (file.getAbsolutePath().startsWith(destination.getAbsolutePath())) { |
||||
mkParentDirs(file); |
||||
try (OutputStream out = new FileOutputStream(file)) { |
||||
StreamUtils.copy(zip, out); |
||||
} |
||||
Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime()); |
||||
} |
||||
} |
||||
|
||||
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 folder " + file); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.stream.Stream; |
||||
|
||||
/** |
||||
* Implicit {@code 'help'} command. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class HelpCommand extends Command { |
||||
|
||||
private final Context context; |
||||
|
||||
private final List<Command> commands; |
||||
|
||||
HelpCommand(Context context, List<Command> commands) { |
||||
super("help", "Help about any command", Options.none(), Parameters.of("[<command]")); |
||||
this.context = context; |
||||
this.commands = commands; |
||||
} |
||||
|
||||
@Override |
||||
protected void run(Map<Option, String> options, List<String> parameters) { |
||||
run(System.out, options, parameters); |
||||
} |
||||
|
||||
void run(PrintStream out, Map<Option, String> options, List<String> parameters) { |
||||
Command command = (!parameters.isEmpty()) ? Command.find(this.commands, parameters.get(0)) : null; |
||||
if (command != null) { |
||||
printCommandHelp(out, command); |
||||
return; |
||||
} |
||||
printUsageAndCommands(out); |
||||
} |
||||
|
||||
private void printCommandHelp(PrintStream out, Command command) { |
||||
out.println(command.getDescription()); |
||||
out.println(); |
||||
out.println("Usage:"); |
||||
out.println(" " + getJavaCommand() + " " + getUsage(command)); |
||||
if (!command.getOptions().isEmpty()) { |
||||
out.println(); |
||||
out.println("Options:"); |
||||
int maxNameLength = getMaxLength(0, command.getOptions().stream().map(Option::getNameAndValueDescription)); |
||||
command.getOptions().stream().forEach((option) -> printOptionSummary(out, option, maxNameLength)); |
||||
} |
||||
} |
||||
|
||||
private void printOptionSummary(PrintStream out, Option option, int padding) { |
||||
out.println(String.format(" --%-" + padding + "s %s", option.getNameAndValueDescription(), |
||||
option.getDescription())); |
||||
} |
||||
|
||||
private String getUsage(Command command) { |
||||
StringBuilder usage = new StringBuilder(); |
||||
usage.append(command.getName()); |
||||
if (!command.getOptions().isEmpty()) { |
||||
usage.append(" [options]"); |
||||
} |
||||
command.getParameters().getDescriptions().forEach((param) -> usage.append(" " + param)); |
||||
return usage.toString(); |
||||
} |
||||
|
||||
private void printUsageAndCommands(PrintStream out) { |
||||
out.println("Usage:"); |
||||
out.println(" " + getJavaCommand()); |
||||
out.println(); |
||||
out.println("Available commands:"); |
||||
int maxNameLength = getMaxLength(getName().length(), this.commands.stream().map(Command::getName)); |
||||
this.commands.forEach((command) -> printCommandSummary(out, command, maxNameLength)); |
||||
printCommandSummary(out, this, maxNameLength); |
||||
} |
||||
|
||||
private int getMaxLength(int minimum, Stream<String> strings) { |
||||
return Math.max(minimum, strings.mapToInt(String::length).max().orElse(0)); |
||||
} |
||||
|
||||
private void printCommandSummary(PrintStream out, Command command, int padding) { |
||||
out.println(String.format(" %-" + padding + "s %s", command.getName(), command.getDescription())); |
||||
} |
||||
|
||||
private String getJavaCommand() { |
||||
return "java -Djarmode=layertools -jar " + this.context.getJarFile().getName(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
/** |
||||
* {@link Layers} implementation that uses implicit rules to slice the application. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ImplicitLayers implements Layers { |
||||
|
||||
private static final String DEPENDENCIES_LAYER = "dependencies"; |
||||
|
||||
private static final String SNAPSHOT_DEPENDENCIES_LAYER = "snapshot-dependencies"; |
||||
|
||||
private static final String RESOURCES_LAYER = "resources"; |
||||
|
||||
private static final String APPLICATION_LAYER = "application"; |
||||
|
||||
private static final List<String> LAYERS; |
||||
static { |
||||
List<String> layers = new ArrayList<>(); |
||||
layers.add(DEPENDENCIES_LAYER); |
||||
layers.add(SNAPSHOT_DEPENDENCIES_LAYER); |
||||
layers.add(RESOURCES_LAYER); |
||||
layers.add(APPLICATION_LAYER); |
||||
LAYERS = Collections.unmodifiableList(layers); |
||||
} |
||||
|
||||
private static final String[] CLASS_LOCATIONS = { "", "BOOT-INF/classes/" }; |
||||
|
||||
private static final String[] RESOURCE_LOCATIONS = { "META-INF/resources/", "resources/", "static/", "public/" }; |
||||
|
||||
@Override |
||||
public Iterator<String> iterator() { |
||||
return LAYERS.iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public String getLayer(ZipEntry entry) { |
||||
return getLayer(entry.getName()); |
||||
} |
||||
|
||||
String getLayer(String name) { |
||||
if (name.endsWith("SNAPSHOT.jar")) { |
||||
return SNAPSHOT_DEPENDENCIES_LAYER; |
||||
} |
||||
if (name.endsWith(".jar")) { |
||||
return DEPENDENCIES_LAYER; |
||||
} |
||||
if (!name.endsWith(".class")) { |
||||
for (String classLocation : CLASS_LOCATIONS) { |
||||
for (String resourceLocation : RESOURCE_LOCATIONS) { |
||||
if (name.startsWith(classLocation + resourceLocation)) { |
||||
return RESOURCES_LAYER; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return APPLICATION_LAYER; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.FileNotFoundException; |
||||
import java.io.IOException; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.jar.JarFile; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
import java.util.stream.Collectors; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StreamUtils; |
||||
|
||||
/** |
||||
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class IndexedLayers implements Layers { |
||||
|
||||
private static final String APPLICATION_LAYER = "application"; |
||||
|
||||
private static final String SPRING_BOOT_APPLICATION_LAYER = "springbootapplication"; |
||||
|
||||
private static final Pattern LAYER_PATTERN = Pattern.compile("^BOOT-INF\\/layers\\/([a-zA-Z0-9-]+)\\/.*$"); |
||||
|
||||
private List<String> layers; |
||||
|
||||
IndexedLayers(String indexFile) { |
||||
String[] lines = indexFile.split("\n"); |
||||
this.layers = Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()) |
||||
.collect(Collectors.toCollection(ArrayList::new)); |
||||
Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); |
||||
if (!this.layers.contains(APPLICATION_LAYER)) { |
||||
this.layers.add(0, SPRING_BOOT_APPLICATION_LAYER); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<String> iterator() { |
||||
return this.layers.iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public String getLayer(ZipEntry entry) { |
||||
String name = entry.getName(); |
||||
Matcher matcher = LAYER_PATTERN.matcher(name); |
||||
if (matcher.matches()) { |
||||
String layer = matcher.group(1); |
||||
Assert.state(this.layers.contains(layer), "Unexpected layer '" + layer + "'"); |
||||
return layer; |
||||
} |
||||
return this.layers.contains(APPLICATION_LAYER) ? APPLICATION_LAYER : SPRING_BOOT_APPLICATION_LAYER; |
||||
} |
||||
|
||||
/** |
||||
* Get an {@link IndexedLayers} instance of possible. |
||||
* @param context the context |
||||
* @return an {@link IndexedLayers} instance or {@code null} if this not a layered |
||||
* jar. |
||||
*/ |
||||
static IndexedLayers get(Context context) { |
||||
try { |
||||
try (JarFile jarFile = new JarFile(context.getJarFile())) { |
||||
ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx"); |
||||
if (entry != null) { |
||||
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); |
||||
return new IndexedLayers(indexFile); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
catch (FileNotFoundException ex) { |
||||
return null; |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* 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.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 |
||||
* @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(new ArrayDeque<>(Arrays.asList(args))); |
||||
} |
||||
|
||||
private void run(Deque<String> args) { |
||||
if (!args.isEmpty()) { |
||||
Command command = Command.find(this.commands, args.removeFirst()); |
||||
if (command != null) { |
||||
command.run(args); |
||||
return; |
||||
} |
||||
} |
||||
this.help.run(args); |
||||
} |
||||
|
||||
static List<Command> getCommands(Context context) { |
||||
List<Command> commands = new ArrayList<Command>(); |
||||
commands.add(new ListCommand(context)); |
||||
commands.add(new ExtractCommand(context)); |
||||
return Collections.unmodifiableList(commands); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.Iterator; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
/** |
||||
* Provides information about the jar layers. |
||||
* |
||||
* @author Phillip Webb |
||||
* @see ExtractCommand |
||||
* @see ListCommand |
||||
*/ |
||||
interface Layers extends Iterable<String> { |
||||
|
||||
/** |
||||
* Return the jar layers in the order that they should be added (starting with the |
||||
* least frequently changed layer). |
||||
*/ |
||||
@Override |
||||
Iterator<String> iterator(); |
||||
|
||||
/** |
||||
* Return the layer that a given entry is in. |
||||
* @param entry the entry to check |
||||
* @return the layer that the entry is in |
||||
*/ |
||||
String getLayer(ZipEntry entry); |
||||
|
||||
/** |
||||
* Return a {@link Layers} instance for the currently running application. |
||||
* @param context the command context |
||||
* @return a new layers instance |
||||
*/ |
||||
static Layers get(Context context) { |
||||
IndexedLayers indexedLayers = IndexedLayers.get(context); |
||||
return (indexedLayers != null) ? indexedLayers : new ImplicitLayers(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.PrintStream; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* The {@code 'list-layers'} tools command. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ListCommand extends Command { |
||||
|
||||
private Context context; |
||||
|
||||
ListCommand(Context context) { |
||||
super("list", "List layers from the jar that can be extracted", Options.none(), Parameters.none()); |
||||
this.context = context; |
||||
} |
||||
|
||||
@Override |
||||
protected void run(Map<Option, String> options, List<String> parameters) { |
||||
printLayers(Layers.get(this.context), System.out); |
||||
} |
||||
|
||||
void printLayers(Layers layers, PrintStream out) { |
||||
layers.forEach(out::println); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
/** |
||||
* JarMode support for layertools. |
||||
*/ |
||||
package org.springframework.boot.layertools; |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.loader.jarmode.JarMode=\ |
||||
org.springframework.boot.layertools.LayerToolsJarMode |
||||
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.ArrayDeque; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.layertools.Command.Option; |
||||
import org.springframework.boot.layertools.Command.Options; |
||||
import org.springframework.boot.layertools.Command.Parameters; |
||||
|
||||
import static org.assertj.core.api.Assertions.as; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link Command}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class CommandTests { |
||||
|
||||
private static final Option VERBOSE_FLAG = Option.flag("verbose", "Verbose output"); |
||||
|
||||
private static final Option LOG_LEVEL_OPTION = Option.of("log-level", "Logging level (debug or info)", "string"); |
||||
|
||||
@Test |
||||
void getNameReturnsName() { |
||||
TestCommand command = new TestCommand("test"); |
||||
assertThat(command.getName()).isEqualTo("test"); |
||||
} |
||||
|
||||
@Test |
||||
void getDescriptionReturnsDescription() { |
||||
TestCommand command = new TestCommand("test", "Test description", Options.none(), Parameters.none()); |
||||
assertThat(command.getDescription()).isEqualTo("Test description"); |
||||
} |
||||
|
||||
@Test |
||||
void getOptionsReturnsOptions() { |
||||
Options options = Options.of(LOG_LEVEL_OPTION); |
||||
TestCommand command = new TestCommand("test", "test", options, Parameters.none()); |
||||
assertThat(command.getOptions()).isEqualTo(options); |
||||
} |
||||
|
||||
@Test |
||||
void getParametersReturnsParameters() { |
||||
Parameters parameters = Parameters.of("[<param>]"); |
||||
TestCommand command = new TestCommand("test", "test", Options.none(), parameters); |
||||
assertThat(command.getParameters()).isEqualTo(parameters); |
||||
} |
||||
|
||||
@Test |
||||
void runWithOptionsAndParametersParsesOptionsAndParameters() { |
||||
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION); |
||||
run(command, "--verbose", "--log-level", "test1", "test2", "test3"); |
||||
assertThat(command.getRunOptions()).containsEntry(VERBOSE_FLAG, null); |
||||
assertThat(command.getRunOptions()).containsEntry(LOG_LEVEL_OPTION, "test1"); |
||||
assertThat(command.getRunParameters()).containsExactly("test2", "test3"); |
||||
} |
||||
|
||||
@Test |
||||
void findWhenNameMatchesReturnsCommand() { |
||||
TestCommand test1 = new TestCommand("test1"); |
||||
TestCommand test2 = new TestCommand("test2"); |
||||
List<Command> commands = Arrays.asList(test1, test2); |
||||
assertThat(Command.find(commands, "test1")).isEqualTo(test1); |
||||
assertThat(Command.find(commands, "test2")).isEqualTo(test2); |
||||
} |
||||
|
||||
@Test |
||||
void findWhenNameDoesNotMatchReturnsNull() { |
||||
TestCommand test1 = new TestCommand("test1"); |
||||
TestCommand test2 = new TestCommand("test2"); |
||||
List<Command> commands = Arrays.asList(test1, test2); |
||||
assertThat(Command.find(commands, "test3")).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void parametersOfCreatesParametersInstance() { |
||||
Parameters parameters = Parameters.of("test1", "test2"); |
||||
assertThat(parameters.getDescriptions()).containsExactly("test1", "test2"); |
||||
} |
||||
|
||||
@Test |
||||
void optionsNoneReturnsEmptyOptions() { |
||||
Options options = Options.none(); |
||||
assertThat(options).extracting("values", as(InstanceOfAssertFactories.ARRAY)).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void optionsOfReturnsOptions() { |
||||
Option option = Option.of("test", "value description", "description"); |
||||
Options options = Options.of(option); |
||||
assertThat(options).extracting("values", as(InstanceOfAssertFactories.ARRAY)).containsExactly(option); |
||||
} |
||||
|
||||
@Test |
||||
void optionFlagCreatesFlagOption() { |
||||
Option option = Option.flag("test", "description"); |
||||
assertThat(option.getName()).isEqualTo("test"); |
||||
assertThat(option.getDescription()).isEqualTo("description"); |
||||
assertThat(option.getValueDescription()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void optionOfCreatesValueOption() { |
||||
Option option = Option.of("test", "value description", "description"); |
||||
assertThat(option.getName()).isEqualTo("test"); |
||||
assertThat(option.getDescription()).isEqualTo("description"); |
||||
assertThat(option.getValueDescription()).isEqualTo("value description"); |
||||
} |
||||
|
||||
private void run(TestCommand command, String... args) { |
||||
command.run(new ArrayDeque<>(Arrays.asList(args))); |
||||
} |
||||
|
||||
static class TestCommand extends Command { |
||||
|
||||
private Map<Option, String> runOptions; |
||||
|
||||
private List<String> runParameters; |
||||
|
||||
TestCommand(String name, Option... options) { |
||||
this(name, "test", Options.of(options), Parameters.none()); |
||||
} |
||||
|
||||
TestCommand(String name, String description, Options options, Parameters parameters) { |
||||
super(name, description, options, parameters); |
||||
} |
||||
|
||||
@Override |
||||
protected void run(Map<Option, String> options, List<String> parameters) { |
||||
this.runOptions = options; |
||||
this.runParameters = parameters; |
||||
} |
||||
|
||||
Map<Option, String> getRunOptions() { |
||||
return this.runOptions; |
||||
} |
||||
|
||||
List<String> getRunParameters() { |
||||
return this.runParameters; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.io.TempDir; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
|
||||
/** |
||||
* Tests for {@link Context}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ContextTests { |
||||
|
||||
@TempDir |
||||
File temp; |
||||
|
||||
@Test |
||||
void createWhenSourceIsNullThrowsException() { |
||||
assertThatIllegalStateException().isThrownBy(() -> new Context(null, this.temp)) |
||||
.withMessage("Unable to find source JAR"); |
||||
} |
||||
|
||||
@Test |
||||
void createWhenSourceIsFolderThrowsException() { |
||||
File folder = new File(this.temp, "test"); |
||||
folder.mkdir(); |
||||
assertThatIllegalStateException().isThrownBy(() -> new Context(folder, this.temp)) |
||||
.withMessage("Unable to find source JAR"); |
||||
} |
||||
|
||||
@Test |
||||
void createWhenSourceIsNotJarThrowsException() throws Exception { |
||||
File zip = new File(this.temp, "test.zip"); |
||||
Files.createFile(zip.toPath()); |
||||
assertThatIllegalStateException().isThrownBy(() -> new Context(zip, this.temp)) |
||||
.withMessage("Unable to find source JAR"); |
||||
} |
||||
|
||||
@Test |
||||
void getJarFileReturnsJar() throws Exception { |
||||
File jar = new File(this.temp, "test.jar"); |
||||
Files.createFile(jar.toPath()); |
||||
Context context = new Context(jar, this.temp); |
||||
assertThat(context.getJarFile()).isEqualTo(jar); |
||||
} |
||||
|
||||
@Test |
||||
void getWorkingDirectoryReturnsWorkingDir() throws IOException { |
||||
File jar = new File(this.temp, "test.jar"); |
||||
Files.createFile(jar.toPath()); |
||||
Context context = new Context(jar, this.temp); |
||||
assertThat(context.getWorkingDir()).isEqualTo(this.temp); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
void getRelativePathReturnsRelativePath() throws Exception { |
||||
File target = new File(this.temp, "target"); |
||||
target.mkdir(); |
||||
File jar = new File(target, "test.jar"); |
||||
Files.createFile(jar.toPath()); |
||||
Context context = new Context(jar, this.temp); |
||||
assertThat(context.getRelativeJarDir()).isEqualTo("target"); |
||||
} |
||||
|
||||
@Test |
||||
void getRelativePathWhenWorkingDirReturnsNull() throws Exception { |
||||
File jar = new File(this.temp, "test.jar"); |
||||
Files.createFile(jar.toPath()); |
||||
Context context = new Context(jar, this.temp); |
||||
assertThat(context.getRelativeJarDir()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void getRelativePathWhenCannotBeDeducedReturnsNull() throws Exception { |
||||
File folder1 = new File(this.temp, "folder1"); |
||||
folder1.mkdir(); |
||||
File folder2 = new File(this.temp, "folder1"); |
||||
folder2.mkdir(); |
||||
File jar = new File(folder1, "test.jar"); |
||||
Files.createFile(jar.toPath()); |
||||
Context context = new Context(jar, folder2); |
||||
assertThat(context.getRelativeJarDir()).isNull(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
import java.util.zip.ZipEntry; |
||||
import java.util.zip.ZipOutputStream; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.io.TempDir; |
||||
import org.mockito.Mock; |
||||
import org.mockito.MockitoAnnotations; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
|
||||
/** |
||||
* Tests for {@link ExtractCommand}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ExtractCommandTests { |
||||
|
||||
@TempDir |
||||
File temp; |
||||
|
||||
@Mock |
||||
private Context context; |
||||
|
||||
private File jarFile; |
||||
|
||||
private File extract; |
||||
|
||||
private Layers layers = new TestLayers(); |
||||
|
||||
private ExtractCommand command; |
||||
|
||||
@BeforeEach |
||||
void setup() throws Exception { |
||||
MockitoAnnotations.initMocks(this); |
||||
this.jarFile = createJarFile("test.jar"); |
||||
this.extract = new File(this.temp, "extract"); |
||||
this.extract.mkdir(); |
||||
given(this.context.getJarFile()).willReturn(this.jarFile); |
||||
given(this.context.getWorkingDir()).willReturn(this.extract); |
||||
this.command = new ExtractCommand(this.context, this.layers); |
||||
} |
||||
|
||||
@Test |
||||
void runExtractsLayers() throws Exception { |
||||
this.command.run(Collections.emptyMap(), Collections.emptyList()); |
||||
assertThat(this.extract.list()).containsOnly("a", "b", "c"); |
||||
assertThat(new File(this.extract, "a/a/a.jar")).exists(); |
||||
assertThat(new File(this.extract, "b/b/b.jar")).exists(); |
||||
assertThat(new File(this.extract, "c/c/c.jar")).exists(); |
||||
} |
||||
|
||||
@Test |
||||
void runWhenHasDestinationOptionExtractsLayers() { |
||||
File out = new File(this.extract, "out"); |
||||
this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()), |
||||
Collections.emptyList()); |
||||
assertThat(this.extract.list()).containsOnly("out"); |
||||
assertThat(new File(this.extract, "out/a/a/a.jar")).exists(); |
||||
assertThat(new File(this.extract, "out/b/b/b.jar")).exists(); |
||||
assertThat(new File(this.extract, "out/c/c/c.jar")).exists(); |
||||
} |
||||
|
||||
@Test |
||||
void runWhenHasLayerParamsExtractsLimitedLayers() { |
||||
this.command.run(Collections.emptyMap(), Arrays.asList("a", "c")); |
||||
assertThat(this.extract.list()).containsOnly("a", "c"); |
||||
assertThat(new File(this.extract, "a/a/a.jar")).exists(); |
||||
assertThat(new File(this.extract, "c/c/c.jar")).exists(); |
||||
} |
||||
|
||||
private File createJarFile(String name) throws IOException { |
||||
File file = new File(this.temp, name); |
||||
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) { |
||||
out.putNextEntry(new ZipEntry("a/")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("a/a.jar")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("b/")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("b/b.jar")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("c/")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("c/c.jar")); |
||||
out.closeEntry(); |
||||
out.putNextEntry(new ZipEntry("d/")); |
||||
out.closeEntry(); |
||||
} |
||||
return file; |
||||
} |
||||
|
||||
private static class TestLayers implements Layers { |
||||
|
||||
@Override |
||||
public Iterator<String> iterator() { |
||||
return Arrays.asList("a", "b", "c").iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public String getLayer(ZipEntry entry) { |
||||
if (entry.getName().startsWith("a")) { |
||||
return "a"; |
||||
} |
||||
if (entry.getName().startsWith("b")) { |
||||
return "b"; |
||||
} |
||||
return "c"; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
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; |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
Context context = mock(Context.class); |
||||
given(context.getJarFile()).willReturn(new File("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.emptyMap(), Collections.emptyList()); |
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt"); |
||||
} |
||||
|
||||
@Test |
||||
void runWhenHasNoCommandParameterPrintsUsage() { |
||||
this.command.run(this.out, Collections.emptyMap(), Arrays.asList("extract")); |
||||
System.out.println(this.out); |
||||
assertThat(this.out).hasSameContentAsResource("help-extract-output.txt"); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.zip.ZipEntry; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link ImplicitLayers}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ImplicitLayersTests { |
||||
|
||||
private Layers layers = new ImplicitLayers(); |
||||
|
||||
@Test |
||||
void iteratorReturnsLayers() { |
||||
assertThat(this.layers).containsExactly("dependencies", "snapshot-dependencies", "resources", "application"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenSnapshotJarReturnsSnapshotDependencies() { |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/lib/mylib-SNAPSHOT.jar"))) |
||||
.isEqualTo("snapshot-dependencies"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenNonSnapshotJarReturnsDependencies() { |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/lib/mylib.jar"))).isEqualTo("dependencies"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenLoaderClassReturnsApplication() { |
||||
assertThat(this.layers.getLayer(zipEntry("org/springframework/boot/loader/Example.class"))) |
||||
.isEqualTo("application"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenStaticResourceReturnsResources() { |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/META-INF/resources/image.gif"))) |
||||
.isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/resources/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/static/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/public/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("META-INF/resources/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("resources/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("static/image.gif"))).isEqualTo("resources"); |
||||
assertThat(this.layers.getLayer(zipEntry("public/image.gif"))).isEqualTo("resources"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenRegularClassReturnsApplication() { |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/com.example/App.class"))).isEqualTo("application"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenClassResourceReturnsApplication() { |
||||
assertThat(this.layers.getLayer(zipEntry("BOOT-INF/classes/application.properties"))).isEqualTo("application"); |
||||
} |
||||
|
||||
private ZipEntry zipEntry(String name) { |
||||
return new ZipEntry(name); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.util.zip.ZipEntry; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link IndexedLayers}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class IndexedLayersTests { |
||||
|
||||
@Test |
||||
void createWhenIndexFileIsEmptyThrowsException() { |
||||
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ")) |
||||
.withMessage("Empty layer index file loaded"); |
||||
} |
||||
|
||||
@Test |
||||
void createWhenIndexFileHasNoApplicationLayerAddSpringBootApplication() { |
||||
IndexedLayers layers = new IndexedLayers("test"); |
||||
assertThat(layers).contains("springbootapplication"); |
||||
} |
||||
|
||||
@Test |
||||
void iteratorReturnsLayers() { |
||||
IndexedLayers layers = new IndexedLayers("test\napplication"); |
||||
assertThat(layers).containsExactly("test", "application"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenMatchesLayerPatterReturnsLayer() { |
||||
IndexedLayers layers = new IndexedLayers("test"); |
||||
assertThat(layers.getLayer(mockEntry("BOOT-INF/layers/test/lib/file.jar"))).isEqualTo("test"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenMatchesLayerPatterForMissingLayerThrowsException() { |
||||
IndexedLayers layers = new IndexedLayers("test"); |
||||
assertThatIllegalStateException() |
||||
.isThrownBy(() -> layers.getLayer(mockEntry("BOOT-INF/layers/missing/lib/file.jar"))) |
||||
.withMessage("Unexpected layer 'missing'"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenDoesNotMatchLayerPatternReturnsApplication() { |
||||
IndexedLayers layers = new IndexedLayers("test\napplication"); |
||||
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); |
||||
} |
||||
|
||||
@Test |
||||
void getLayerWhenDoesNotMatchLayerPatternAndHasNoApplicationLayerReturnsSpringApplication() { |
||||
IndexedLayers layers = new IndexedLayers("test"); |
||||
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("springbootapplication"); |
||||
} |
||||
|
||||
private ZipEntry mockEntry(String name) { |
||||
ZipEntry entry = mock(ZipEntry.class); |
||||
given(entry.getName()).willReturn(name); |
||||
return entry; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.File; |
||||
import java.io.PrintStream; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link LayerToolsJarMode}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class LayerToolsJarModeTests { |
||||
|
||||
private static final String[] NO_ARGS = {}; |
||||
|
||||
private TestPrintStream out; |
||||
|
||||
private PrintStream systemOut; |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
Context context = mock(Context.class); |
||||
given(context.getJarFile()).willReturn(new File("test.jar")); |
||||
this.out = new TestPrintStream(this); |
||||
this.systemOut = System.out; |
||||
System.setOut(this.out); |
||||
LayerToolsJarMode.Runner.contextOverride = context; |
||||
} |
||||
|
||||
@AfterEach |
||||
void restore() { |
||||
System.setOut(this.systemOut); |
||||
LayerToolsJarMode.Runner.contextOverride = null; |
||||
} |
||||
|
||||
@Test |
||||
void mainWithNoParamersShowsHelp() { |
||||
new LayerToolsJarMode().run("layertools", NO_ARGS); |
||||
assertThat(this.out).hasSameContentAsResource("help-output.txt"); |
||||
} |
||||
|
||||
@Test |
||||
void mainWithArgRunsCommand() { |
||||
new LayerToolsJarMode().run("layertools", new String[] { "list" }); |
||||
assertThat(this.out).hasSameContentAsResource("list-output.txt"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link ListCommand}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ListCommandTests { |
||||
|
||||
private ListCommand command; |
||||
|
||||
private TestPrintStream out; |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
this.command = new ListCommand(mock(Context.class)); |
||||
this.out = new TestPrintStream(this); |
||||
} |
||||
|
||||
@Test |
||||
void listLayersShouldListLayers() { |
||||
this.command.printLayers(new ImplicitLayers(), this.out); |
||||
assertThat(this.out).hasSameContentAsResource("list-output.txt"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
/* |
||||
* 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.layertools; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.PrintStream; |
||||
import java.nio.charset.StandardCharsets; |
||||
|
||||
import org.assertj.core.api.AbstractAssert; |
||||
import org.assertj.core.api.AssertProvider; |
||||
import org.assertj.core.api.Assertions; |
||||
|
||||
import org.springframework.boot.layertools.TestPrintStream.PrintStreamAssert; |
||||
import org.springframework.util.FileCopyUtils; |
||||
|
||||
/** |
||||
* {@link PrintStream} that can be used for testing. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class TestPrintStream extends PrintStream implements AssertProvider<PrintStreamAssert> { |
||||
|
||||
private Class<? extends Object> testClass; |
||||
|
||||
TestPrintStream(Object testInstance) { |
||||
super(new ByteArrayOutputStream()); |
||||
this.testClass = testInstance.getClass(); |
||||
} |
||||
|
||||
@Override |
||||
public PrintStreamAssert assertThat() { |
||||
return new PrintStreamAssert(this); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.out.toString(); |
||||
} |
||||
|
||||
static final class PrintStreamAssert extends AbstractAssert<PrintStreamAssert, TestPrintStream> { |
||||
|
||||
private PrintStreamAssert(TestPrintStream actual) { |
||||
super(actual, PrintStreamAssert.class); |
||||
} |
||||
|
||||
void hasSameContentAsResource(String resource) { |
||||
try { |
||||
InputStream stream = this.actual.testClass.getResourceAsStream(resource); |
||||
String content = FileCopyUtils.copyToString(new InputStreamReader(stream, StandardCharsets.UTF_8)); |
||||
Assertions.assertThat(this.actual.toString()).isEqualTo(content); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
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,7 @@
@@ -0,0 +1,7 @@
|
||||
Usage: |
||||
java -Djarmode=layertools -jar test.jar |
||||
|
||||
Available commands: |
||||
list List layers from the jar that can be extracted |
||||
extract Extracts layers from the jar for image creation |
||||
help Help about any command |
||||
Loading…
Reference in new issue