Add layertools jarmode
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
parent
73a42050d6
commit
e513fe4666
@ -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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
org.springframework.boot.loader.jarmode.JarMode=\
|
||||||
|
org.springframework.boot.layertools.LayerToolsJarMode
|
@ -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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
|
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 @@
|
|||||||
|
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
|
@ -0,0 +1,4 @@
|
|||||||
|
dependencies
|
||||||
|
snapshot-dependencies
|
||||||
|
resources
|
||||||
|
application
|
Loading…
Reference in New Issue