Refine layer customization for Maven and Gradle

Simplify layer customization logic for both Maven and Gradle and
refactor some internals of the Gradle plugin.

Both Maven and Gradle now use a simpler customization format that
consists of `application`, `dependencies` and `layer order` sections.
The `application`, `dependencies` configurations support one or more
`into` blocks that are used to select content for a specific layer.

Closes gh-20526
pull/20830/head
Phillip Webb 5 years ago
parent 14718f3e8a
commit 7bc7d86ad4

@ -9,27 +9,17 @@ bootJar {
// tag::layered[] // tag::layered[]
bootJar { bootJar {
layers { layered {
layersOrder "dependencies", "snapshot-dependencies", "application"
libraries {
layerContent("snapshot-dependencies") {
coordinates {
include "*:*:*SNAPSHOT"
}
}
layerContent("dependencies") {
coordinates {
include "*:*"
}
}
}
application { application {
layerContent("application") { intoLayer("application")
locations { }
include "**" dependencies {
} intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
} }
intoLayer("dependencies")
} }
layerOrder "dependencies", "snapshot-dependencies", "application"
} }
} }
// end::layered[] // end::layered[]

@ -7,27 +7,17 @@ plugins {
// tag::layered[] // tag::layered[]
tasks.getByName<BootJar>("bootJar") { tasks.getByName<BootJar>("bootJar") {
layers { layered {
layersOrder("dependencies", "snapshot-dependencies", "application")
libraries {
layerContent("snapshot-dependencies") {
coordinates {
include("*:*:*SNAPSHOT")
}
}
layerContent("dependencies") {
coordinates {
include("*:*")
}
}
}
application { application {
layerContent("application") { intoLayer("application")
locations { }
include("**") dependencies {
} intoLayer("snapshot-dependencies") {
include("*:*:*SNAPSHOT")
} }
intoLayer("dependencies") {
} }
layersOrder("dependencies", "snapshot-dependencies", "application")
} }
} }
// end::layered[] // end::layered[]

@ -9,6 +9,6 @@ bootJar {
// tag::layered[] // tag::layered[]
bootJar { bootJar {
layers() layered()
} }
// end::layered[] // end::layered[]

@ -11,6 +11,6 @@ tasks.getByName<BootJar>("bootJar") {
// tag::layered[] // tag::layered[]
tasks.getByName<BootJar>("bootJar") { tasks.getByName<BootJar>("bootJar") {
layers() layered()
} }
// end::layered[] // end::layered[]

@ -27,7 +27,6 @@ import org.gradle.api.Action;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Task; import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact; import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.ApplicationPlugin;
@ -94,9 +93,6 @@ final class JavaPluginAction implements PluginApplicationAction {
SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets() SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME); .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
bootJar.classpath((Callable<FileCollection>) mainSourceSet::getRuntimeClasspath); bootJar.classpath((Callable<FileCollection>) mainSourceSet::getRuntimeClasspath);
Configuration runtimeClasspathConfiguration = project.getConfigurations()
.getByName(mainSourceSet.getRuntimeClasspathConfigurationName());
runtimeClasspathConfiguration.getIncoming().afterResolve(bootJar::resolvedDependencies);
bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath)); bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath));
}); });
} }

@ -27,6 +27,7 @@ import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.function.Function; import java.util.function.Function;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.RelativePath; import org.gradle.api.file.RelativePath;
@ -34,6 +35,7 @@ import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream; import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
import org.gradle.api.java.archives.Attributes; import org.gradle.api.java.archives.Attributes;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.specs.Specs; import org.gradle.api.specs.Specs;
import org.gradle.api.tasks.WorkResult; import org.gradle.api.tasks.WorkResult;
@ -44,6 +46,9 @@ import org.gradle.api.tasks.util.PatternSet;
* Support class for implementations of {@link BootArchive}. * Support class for implementations of {@link BootArchive}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
* @see BootJar
* @see BootWar
*/ */
class BootArchiveSupport { class BootArchiveSupport {
@ -61,46 +66,71 @@ class BootArchiveSupport {
private final PatternSet requiresUnpack = new PatternSet(); private final PatternSet requiresUnpack = new PatternSet();
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final PatternSet exclusions = new PatternSet(); private final PatternSet exclusions = new PatternSet();
private final String loaderMainClass; private final String loaderMainClass;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private LaunchScriptConfiguration launchScript; private LaunchScriptConfiguration launchScript;
private boolean excludeDevtools = true; private boolean excludeDevtools = true;
BootArchiveSupport(String loaderMainClass, Function<FileCopyDetails, ZipCompression> compressionResolver) { BootArchiveSupport(String loaderMainClass, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver) {
this.loaderMainClass = loaderMainClass; this.loaderMainClass = loaderMainClass;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver; this.compressionResolver = compressionResolver;
this.requiresUnpack.include(Specs.satisfyNone()); this.requiresUnpack.include(Specs.satisfyNone());
configureExclusions(); configureExclusions();
} }
void configureManifest(Jar jar, String mainClassName, String springBootClasses, String springBootLib) { void configureManifest(Manifest manifest, String mainClass, String classes, String lib, String classPathIndex,
Attributes attributes = jar.getManifest().getAttributes(); String layersIndex) {
Attributes attributes = manifest.getAttributes();
attributes.putIfAbsent("Main-Class", this.loaderMainClass); attributes.putIfAbsent("Main-Class", this.loaderMainClass);
attributes.putIfAbsent("Start-Class", mainClassName); attributes.putIfAbsent("Start-Class", mainClass);
attributes.computeIfAbsent("Spring-Boot-Version", (key) -> determineSpringBootVersion()); attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion());
attributes.putIfAbsent("Spring-Boot-Classes", springBootClasses); if (classes != null) {
attributes.putIfAbsent("Spring-Boot-Lib", springBootLib); attributes.putIfAbsent("Spring-Boot-Classes", classes);
}
if (lib != null) {
attributes.putIfAbsent("Spring-Boot-Lib", lib);
}
if (classPathIndex != null) {
attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex);
}
if (layersIndex != null) {
attributes.putIfAbsent("Spring-Boot-Layers-Index", layersIndex);
}
} }
private String determineSpringBootVersion() { private String determineSpringBootVersion() {
String implementationVersion = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
return (implementationVersion != null) ? implementationVersion : "unknown"; return (version != null) ? version : "unknown";
} }
CopyAction createCopyAction(Jar jar) { CopyAction createCopyAction(Jar jar) {
CopyAction copyAction = new BootZipCopyAction(jar.getArchiveFile().get().getAsFile(), return createCopyAction(jar, null, false);
jar.isPreserveFileTimestamps(), isUsingDefaultLoader(jar), this.requiresUnpack.getAsSpec(), }
this.exclusions.getAsExcludeSpec(), this.launchScript, this.compressionResolver,
jar.getMetadataCharset()); CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, boolean includeLayerTools) {
if (!jar.isReproducibleFileOrder()) { File output = jar.getArchiveFile().get().getAsFile();
return copyAction; Manifest manifest = jar.getManifest();
} boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
return new ReproducibleOrderingCopyAction(copyAction); boolean includeDefaultLoader = isUsingDefaultLoader(jar);
Spec<FileTreeElement> requiresUnpack = this.requiresUnpack.getAsSpec();
Spec<FileTreeElement> exclusions = this.exclusions.getAsExcludeSpec();
LaunchScriptConfiguration launchScript = this.launchScript;
Spec<FileCopyDetails> librarySpec = this.librarySpec;
Function<FileCopyDetails, ZipCompression> compressionResolver = this.compressionResolver;
String encoding = jar.getMetadataCharset();
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, includeDefaultLoader,
includeLayerTools, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, encoding,
layerResolver);
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;
} }
private boolean isUsingDefaultLoader(Jar jar) { private boolean isUsingDefaultLoader(Jar jar) {
@ -132,7 +162,19 @@ class BootArchiveSupport {
configureExclusions(); configureExclusions();
} }
boolean isZip(File file) { void excludeNonZipLibraryFiles(FileCopyDetails details) {
if (this.librarySpec.isSatisfiedBy(details)) {
excludeNonZipFiles(details);
}
}
void excludeNonZipFiles(FileCopyDetails details) {
if (!isZip(details.getFile())) {
details.exclude();
}
}
private boolean isZip(File file) {
try { try {
try (FileInputStream fileInputStream = new FileInputStream(file)) { try (FileInputStream fileInputStream = new FileInputStream(file)) {
return isZip(fileInputStream); return isZip(fileInputStream);
@ -160,6 +202,17 @@ class BootArchiveSupport {
this.exclusions.setExcludes(excludes); this.exclusions.setExcludes(excludes);
} }
void moveModuleInfoToRoot(CopySpec spec) {
spec.filesMatching("module-info.class", BootArchiveSupport::moveToRoot);
}
private static void moveToRoot(FileCopyDetails details) {
details.setRelativePath(details.getRelativeSourcePath());
}
/**
* {@link CopyAction} variant that sorts entries to ensure reproducible ordering.
*/
private static final class ReproducibleOrderingCopyAction implements CopyAction { private static final class ReproducibleOrderingCopyAction implements CopyAction {
private final CopyAction delegate; private final CopyAction delegate;

@ -16,45 +16,24 @@
package org.springframework.boot.gradle.tasks.bundling; package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import groovy.lang.Closure;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.artifacts.ArtifactCollection; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ResolvableDependencies;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.CopySpec; import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.FileTreeElement;
import org.gradle.api.internal.file.copy.CopyAction; import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Jar;
import org.gradle.util.ConfigureUtil;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Layers;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.boot.loader.tools.layer.CustomLayers;
import org.springframework.util.FileCopyUtils;
/** /**
* A custom {@link Jar} task that produces a Spring Boot executable jar. * A custom {@link Jar} task that produces a Spring Boot executable jar.
@ -62,82 +41,88 @@ import org.springframework.util.FileCopyUtils;
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave * @author Madhura Bhave
* @author Scott Frederick * @author Scott Frederick
* @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
*/ */
public class BootJar extends Jar implements BootArchive { public class BootJar extends Jar implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.JarLauncher", private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher";
this::resolveZipCompression);
private final CopySpec bootInf; private static final String CLASSES_FOLDER = "BOOT-INF/classes/";
private String mainClassName; private static final String LIB_FOLDER = "BOOT-INF/lib/";
private FileCollection classpath; private static final String LAYERS_INDEX = "BOOT-INF/layers.idx";
private Layers layers; private static final String CLASSPATH_INDEX = "BOOT-INF/classpath.idx";
private LayerConfiguration layerConfiguration; private final BootArchiveSupport support;
private static final String BOOT_INF_LAYERS = "BOOT-INF/layers"; private final CopySpec bootInfSpec;
private final List<String> dependencies = new ArrayList<>(); private String mainClassName;
private final Map<String, String> coordinatesByFileName = new HashMap<>(); private FileCollection classpath;
private LayeredSpec layered;
/** /**
* Creates a new {@code BootJar} task. * Creates a new {@code BootJar} task.
*/ */
public BootJar() { public BootJar() {
this.bootInf = getProject().copySpec().into("BOOT-INF"); this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
getMainSpec().with(this.bootInf); this.bootInfSpec = getProject().copySpec().into("BOOT-INF");
this.bootInf.into("classes", classpathFiles(File::isDirectory)); configureBootInfSpec(this.bootInfSpec);
this.bootInf.into("lib", classpathFiles(File::isFile)) getMainSpec().with(this.bootInfSpec);
.eachFile((details) -> BootJar.this.dependencies.add(details.getPath())); }
this.bootInf.into("",
(spec) -> spec.from((Callable<File>) () -> createClasspathIndex(BootJar.this.dependencies))); private void configureBootInfSpec(CopySpec bootInfSpec) {
this.bootInf.filesMatching("module-info.class", bootInfSpec.into("classes", fromCallTo(this::classpathDirectories));
bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles);
bootInfSpec.filesMatching("module-info.class",
(details) -> details.setRelativePath(details.getRelativeSourcePath())); (details) -> details.setRelativePath(details.getRelativeSourcePath()));
getRootSpec().eachFile((details) -> {
String pathString = details.getRelativePath().getPathString();
if (pathString.startsWith("BOOT-INF/lib/") && !this.support.isZip(details.getFile())) {
details.exclude();
}
});
} }
private Action<CopySpec> classpathFiles(Spec<File> filter) { private Iterable<File> classpathDirectories() {
return (copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.classpath != null) return classpathEntries(File::isDirectory);
? this.classpath.filter(filter) : Collections.emptyList()); }
private Iterable<File> classpathFiles() {
return classpathEntries(File::isFile);
}
private Iterable<File> classpathEntries(Spec<File> filter) {
return (this.classpath != null) ? this.classpath.filter(filter) : Collections.emptyList();
} }
@Override @Override
public void copy() { public void copy() {
this.support.configureManifest(this, getMainClassName(), "BOOT-INF/classes/", "BOOT-INF/lib/"); if (this.layered != null) {
Attributes attributes = this.getManifest().getAttributes(); this.support.configureManifest(getManifest(), getMainClassName(), null, null, CLASSPATH_INDEX,
if (this.layers != null) { LAYERS_INDEX);
attributes.remove("Spring-Boot-Classes"); }
attributes.remove("Spring-Boot-Lib"); else {
attributes.putIfAbsent("Spring-Boot-Layers-Index", "BOOT-INF/layers.idx"); this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER,
CLASSPATH_INDEX, null);
} }
attributes.putIfAbsent("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx");
super.copy(); super.copy();
} }
private File createClasspathIndex(List<String> dependencies) {
String content = dependencies.stream().map((name) -> name.substring(name.lastIndexOf('/') + 1))
.collect(Collectors.joining("\n", "", "\n"));
File source = getProject().getResources().getText().fromString(content).asFile();
File indexFile = new File(source.getParentFile(), "classpath.idx");
source.renameTo(indexFile);
return indexFile;
}
@Override @Override
protected CopyAction createCopyAction() { protected CopyAction createCopyAction() {
if (this.layered != null) {
LayerResolver layerResolver = new LayerResolver(getConfigurations(), this.layered, this::isLibrary);
boolean includeLayerTools = this.layered.isIncludeLayerTools();
return this.support.createCopyAction(this, layerResolver, includeLayerTools);
}
return this.support.createCopyAction(this); return this.support.createCopyAction(this);
} }
@Internal
protected Iterable<Configuration> getConfigurations() {
return getProject().getConfigurations();
}
@Override @Override
public String getMainClassName() { public String getMainClassName() {
if (this.mainClassName == null) { if (this.mainClassName == null) {
@ -179,110 +164,28 @@ public class BootJar extends Jar implements BootArchive {
action.execute(enableLaunchScriptIfNecessary()); action.execute(enableLaunchScriptIfNecessary());
} }
@Optional
@Nested @Nested
public LayerConfiguration getLayerConfiguration() { @Optional
return this.layerConfiguration; public LayeredSpec getLayered() {
} return this.layered;
/**
* Configures the archive to have layers.
*/
public void layers() {
enableLayers();
}
public void layers(Action<LayerConfiguration> action) {
action.execute(enableLayers());
}
private LayerConfiguration enableLayers() {
if (this.layerConfiguration == null) {
this.layerConfiguration = new LayerConfiguration();
}
return this.layerConfiguration;
}
private void applyLayers() {
if (this.layerConfiguration == null) {
return;
}
if (this.layerConfiguration.getLayersOrder() == null || this.layerConfiguration.getLayersOrder().isEmpty()) {
this.layers = Layers.IMPLICIT;
}
else {
List<Layer> customLayers = this.layerConfiguration.getLayersOrder().stream().map(Layer::new)
.collect(Collectors.toList());
this.layers = new CustomLayers(customLayers, this.layerConfiguration.getApplication(),
this.layerConfiguration.getLibraries());
}
if (this.layerConfiguration.isIncludeLayerTools()) {
this.bootInf.into("lib", (spec) -> spec.from((Callable<File>) () -> {
String jarName = "spring-boot-jarmode-layertools.jar";
InputStream stream = getClass().getClassLoader().getResourceAsStream("META-INF/jarmode/" + jarName);
File taskTmp = new File(getProject().getBuildDir(), "tmp/" + getName());
taskTmp.mkdirs();
File layerToolsJar = new File(taskTmp, jarName);
FileCopyUtils.copy(stream, new FileOutputStream(layerToolsJar));
return layerToolsJar;
}));
}
this.bootInf.eachFile((details) -> {
Layer layer = layerForFileDetails(details);
if (layer != null) {
String relativePath = details.getPath().substring("BOOT-INF/".length());
details.setPath(BOOT_INF_LAYERS + "/" + layer + "/" + relativePath);
}
}).setIncludeEmptyDirs(false);
this.bootInf.into("", (spec) -> spec.from(createLayersIndex()));
} }
private Layer layerForFileDetails(FileCopyDetails details) { public void layered() {
String path = details.getPath(); layered(true);
if (path.startsWith("BOOT-INF/lib/")) {
String coordinates = this.coordinatesByFileName.get(details.getName());
LibraryCoordinates libraryCoordinates = (coordinates != null) ? new LibraryCoordinates(coordinates)
: new LibraryCoordinates("?:?:?");
return this.layers.getLayer(new Library(null, details.getFile(), null, libraryCoordinates, false));
}
if (path.startsWith("BOOT-INF/classes/")) {
return this.layers.getLayer(details.getSourcePath());
}
return null;
} }
private File createLayersIndex() { public void layered(boolean layered) {
try { this.layered = layered ? new LayeredSpec() : null;
StringWriter content = new StringWriter();
BufferedWriter writer = new BufferedWriter(content);
for (Layer layer : this.layers) {
writer.write(layer.toString());
writer.write("\n");
}
writer.flush();
File source = getProject().getResources().getText().fromString(content.toString()).asFile();
File indexFile = new File(source.getParentFile(), "layers.idx");
source.renameTo(indexFile);
return indexFile;
}
catch (IOException ex) {
throw new RuntimeException("Failed to create layers.idx", ex);
}
} }
private void resolveCoordinatesForFiles(ResolvableDependencies resolvableDependencies) { public void layered(Closure<?> closure) {
ArtifactCollection resolvedArtifactResults = resolvableDependencies.getArtifacts(); layered(ConfigureUtil.configureUsing(closure));
Set<ResolvedArtifactResult> artifacts = resolvedArtifactResults.getArtifacts();
artifacts.forEach((artifact) -> this.coordinatesByFileName.put(artifact.getFile().getName(),
artifact.getId().getComponentIdentifier().getDisplayName()));
} }
@Input public void layered(Action<LayeredSpec> action) {
boolean isLayered() { LayeredSpec layered = new LayeredSpec();
return this.layerConfiguration != null; action.execute(layered);
this.layered = layered;
} }
@Override @Override
@ -317,13 +220,6 @@ public class BootJar extends Jar implements BootArchive {
this.support.setExcludeDevtools(excludeDevtools); this.support.setExcludeDevtools(excludeDevtools);
} }
public void resolvedDependencies(ResolvableDependencies resolvableDependencies) {
if (resolvableDependencies != null) {
resolveCoordinatesForFiles(resolvableDependencies);
}
applyLayers();
}
/** /**
* Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF} * Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF}
* directory of the jar. * directory of the jar.
@ -333,7 +229,7 @@ public class BootJar extends Jar implements BootArchive {
@Internal @Internal
public CopySpec getBootInf() { public CopySpec getBootInf() {
CopySpec child = getProject().copySpec(); CopySpec child = getProject().copySpec();
this.bootInf.with(child); this.bootInfSpec.with(child);
return child; return child;
} }
@ -352,30 +248,26 @@ public class BootJar extends Jar implements BootArchive {
} }
/** /**
* Returns the {@link ZipCompression} that should be used when adding the file * Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. * represented by the given {@code details} to the jar. By default, any
* <p> * {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* By default, any file in {@code BOOT-INF/lib/} or * and all other files are {@link ZipCompression#DEFLATED deflated}.
* {@code BOOT-INF/layers/<layer>/lib} is stored and all other files are deflated. * @param details the file copy details
* @param details the details
* @return the compression to use * @return the compression to use
*/ */
protected ZipCompression resolveZipCompression(FileCopyDetails details) { protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String path = details.getRelativePath().getPathString(); return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
for (String prefix : getLibPathPrefixes()) {
if (path.startsWith(prefix)) {
return ZipCompression.STORED;
}
}
return ZipCompression.DEFLATED;
} }
private Set<String> getLibPathPrefixes() { /**
if (this.layers == null) { * Return if the {@link FileCopyDetails} are for a library. By default any file in
return Collections.singleton("BOOT-INF/lib/"); * {@code BOOT-INF/lib} is considered to be a library.
} * @param details the file copy details
return StreamSupport.stream(this.layers.spliterator(), false) * @return {@code true} if the details are for a library
.map((layer) -> "BOOT-INF/layers/" + layer + "/lib/").collect(Collectors.toSet()); */
protected boolean isLibrary(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
return path.startsWith(LIB_FOLDER);
} }
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
@ -387,4 +279,24 @@ public class BootJar extends Jar implements BootArchive {
return launchScript; return launchScript;
} }
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return an action to add the callable to the spec
*/
private static <T> Action<CopySpec> fromCallTo(Callable<T> callable) {
return (spec) -> spec.from(callTo(callable));
}
/**
* Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return the callable
*/
private static <T> Callable<T> callTo(Callable<T> callable) {
return callable;
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,12 +16,12 @@
package org.springframework.boot.gradle.tasks.bundling; package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.FileTreeElement;
@ -35,12 +35,20 @@ import org.gradle.api.tasks.bundling.War;
* A custom {@link War} task that produces a Spring Boot executable war. * A custom {@link War} task that produces a Spring Boot executable war.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
*/ */
public class BootWar extends War implements BootArchive { public class BootWar extends War implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.WarLauncher", private static final String LAUNCHER = "org.springframework.boot.loader.WarLauncher";
this::resolveZipCompression);
private static final String CLASSES_FOLDER = "WEB-INF/classes/";
private static final String LIB_PROVIDED_FOLDER = "WEB-INF/lib-provided/";
private static final String LIB_FOLDER = "WEB-INF/lib/";
private final BootArchiveSupport support;
private String mainClassName; private String mainClassName;
@ -50,23 +58,19 @@ public class BootWar extends War implements BootArchive {
* Creates a new {@code BootWar} task. * Creates a new {@code BootWar} task.
*/ */
public BootWar() { public BootWar() {
getWebInf().into("lib-provided", this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
(copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.providedClasspath != null) getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles));
? this.providedClasspath : Collections.emptyList())); this.support.moveModuleInfoToRoot(getRootSpec());
getRootSpec().filesMatching("module-info.class", getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles);
(details) -> details.setRelativePath(details.getRelativeSourcePath())); }
getRootSpec().eachFile((details) -> {
String pathString = details.getRelativePath().getPathString(); private Object getProvidedLibFiles() {
if ((pathString.startsWith("WEB-INF/lib/") || pathString.startsWith("WEB-INF/lib-provided/")) return (this.providedClasspath != null) ? this.providedClasspath : Collections.emptyList();
&& !this.support.isZip(details.getFile())) {
details.exclude();
}
});
} }
@Override @Override
public void copy() { public void copy() {
this.support.configureManifest(this, getMainClassName(), "WEB-INF/classes/", "WEB-INF/lib/"); this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER, null, null);
super.copy(); super.copy();
} }
@ -171,20 +175,26 @@ public class BootWar extends War implements BootArchive {
} }
/** /**
* Returns the {@link ZipCompression} that should be used when adding the file * Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. * represented by the given {@code details} to the jar. By default, any
* <p> * {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* By default, any file in {@code WEB-INF/lib/} or {@code WEB-INF/lib-provided/} is * and all other files are {@link ZipCompression#DEFLATED deflated}.
* stored and all other files are deflated. * @param details the file copy details
* @param details the details
* @return the compression to use * @return the compression to use
*/ */
protected ZipCompression resolveZipCompression(FileCopyDetails details) { protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String relativePath = details.getRelativePath().getPathString(); return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
if (relativePath.startsWith("WEB-INF/lib/") || relativePath.startsWith("WEB-INF/lib-provided/")) { }
return ZipCompression.STORED;
} /**
return ZipCompression.DEFLATED; * Return if the {@link FileCopyDetails} are for a library. By default any file in
* {@code WEB-INF/lib} or {@code WEB-INF/lib-provided} is considered to be a library.
* @param details the file copy details
* @return {@code true} if the details are for a library
*/
protected boolean isLibrary(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
return path.startsWith(LIB_FOLDER) || path.startsWith(LIB_PROVIDED_FOLDER);
} }
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
@ -196,4 +206,24 @@ public class BootWar extends War implements BootArchive {
return launchScript; return launchScript;
} }
/**
* Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return an action to add the callable to the spec
*/
private static <T> Action<CopySpec> fromCallTo(Callable<T> callable) {
return (spec) -> spec.from(callTo(callable));
}
/**
* Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read.
* @param <T> the result type
* @param callable the callable
* @return the callable
*/
private static <T> Callable<T> callTo(Callable<T> callable) {
return callable;
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,10 +19,15 @@ package org.springframework.boot.gradle.tasks.bundling;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Calendar; import java.io.OutputStreamWriter;
import java.util.GregorianCalendar; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@ -34,11 +39,16 @@ import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.FileTreeElement;
import org.gradle.api.internal.file.copy.CopyAction; import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream; import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.WorkResult; import org.gradle.api.tasks.WorkResult;
import org.gradle.api.tasks.WorkResults;
import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.DefaultLaunchScript;
import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.StreamUtils;
/** /**
* A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war). * A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war).
@ -49,69 +59,84 @@ import org.springframework.boot.loader.tools.FileUtils;
*/ */
class BootZipCopyAction implements CopyAction { class BootZipCopyAction implements CopyAction {
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0) static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC)
.getTimeInMillis(); .toInstant().toEpochMilli();
private final File output; private final File output;
private final Manifest manifest;
private final boolean preserveFileTimestamps; private final boolean preserveFileTimestamps;
private final boolean includeDefaultLoader; private final boolean includeDefaultLoader;
private final boolean includeLayerTools;
private final Spec<FileTreeElement> requiresUnpack; private final Spec<FileTreeElement> requiresUnpack;
private final Spec<FileTreeElement> exclusions; private final Spec<FileTreeElement> exclusions;
private final LaunchScriptConfiguration launchScript; private final LaunchScriptConfiguration launchScript;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver; private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final String encoding; private final String encoding;
BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader, private final LayerResolver layerResolver;
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
LaunchScriptConfiguration launchScript, Function<FileCopyDetails, ZipCompression> compressionResolver, BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader,
String encoding) { boolean includeLayerTools, Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
LayerResolver layerResolver) {
this.output = output; this.output = output;
this.manifest = manifest;
this.preserveFileTimestamps = preserveFileTimestamps; this.preserveFileTimestamps = preserveFileTimestamps;
this.includeDefaultLoader = includeDefaultLoader; this.includeDefaultLoader = includeDefaultLoader;
this.includeLayerTools = includeLayerTools;
this.requiresUnpack = requiresUnpack; this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions; this.exclusions = exclusions;
this.launchScript = launchScript; this.launchScript = launchScript;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver; this.compressionResolver = compressionResolver;
this.encoding = encoding; this.encoding = encoding;
this.layerResolver = layerResolver;
} }
@Override @Override
public WorkResult execute(CopyActionProcessingStream stream) { public WorkResult execute(CopyActionProcessingStream copyActions) {
try { try {
writeArchive(stream); writeArchive(copyActions);
return () -> true; return WorkResults.didWork(true);
} }
catch (IOException ex) { catch (IOException ex) {
throw new GradleException("Failed to create " + this.output, ex); throw new GradleException("Failed to create " + this.output, ex);
} }
} }
private void writeArchive(CopyActionProcessingStream stream) throws IOException { private void writeArchive(CopyActionProcessingStream copyActions) throws IOException {
OutputStream outputStream = new FileOutputStream(this.output); OutputStream output = new FileOutputStream(this.output);
try { try {
writeLaunchScriptIfNecessary(outputStream); writeArchive(copyActions, output);
ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream);
try {
if (this.encoding != null) {
zipOutputStream.setEncoding(this.encoding);
}
Processor processor = new Processor(zipOutputStream);
stream.process(processor::process);
processor.finish();
}
finally {
closeQuietly(zipOutputStream);
}
} }
finally { finally {
closeQuietly(outputStream); closeQuietly(output);
}
}
private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException {
writeLaunchScriptIfNecessary(output);
ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output);
try {
setEncodingIfNecessary(zipOutput);
Processor processor = new Processor(zipOutput);
copyActions.process(processor::process);
processor.finish();
}
finally {
closeQuietly(zipOutput);
} }
} }
@ -131,6 +156,12 @@ class BootZipCopyAction implements CopyAction {
} }
} }
private void setEncodingIfNecessary(ZipArchiveOutputStream zipOutputStream) {
if (this.encoding != null) {
zipOutputStream.setEncoding(this.encoding);
}
}
private void closeQuietly(OutputStream outputStream) { private void closeQuietly(OutputStream outputStream) {
try { try {
outputStream.close(); outputStream.close();
@ -144,17 +175,20 @@ class BootZipCopyAction implements CopyAction {
*/ */
private class Processor { private class Processor {
private ZipArchiveOutputStream outputStream; private ZipArchiveOutputStream out;
private Spec<FileTreeElement> writtenLoaderEntries; private Spec<FileTreeElement> writtenLoaderEntries;
Processor(ZipArchiveOutputStream outputStream) { private Set<String> writtenDirectories = new LinkedHashSet<>();
this.outputStream = outputStream;
private Set<String> writtenLibraries = new LinkedHashSet<>();
Processor(ZipArchiveOutputStream out) {
this.out = out;
} }
void process(FileCopyDetails details) { void process(FileCopyDetails details) {
if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details) if (skipProcessing(details)) {
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) {
return; return;
} }
try { try {
@ -171,8 +205,90 @@ class BootZipCopyAction implements CopyAction {
} }
} }
private boolean skipProcessing(FileCopyDetails details) {
return BootZipCopyAction.this.exclusions.isSatisfiedBy(details)
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details));
}
private void processDirectory(FileCopyDetails details) throws IOException {
String name = getEntryName(details);
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name + '/');
entry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
entry.setTime(time);
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
this.writtenDirectories.add(name);
}
private void processFile(FileCopyDetails details) throws IOException {
String name = getEntryName(details);
long time = getTime(details);
writeParentDirectoriesIfNecessary(name, time);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
entry.setTime(time);
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, entry);
}
this.out.putArchiveEntry(entry);
details.copyTo(this.out);
this.out.closeArchiveEntry();
if (BootZipCopyAction.this.librarySpec.isSatisfiedBy(details)) {
this.writtenLibraries.add(name.substring(name.lastIndexOf('/') + 1));
}
}
private void writeParentDirectoriesIfNecessary(String name, long time) throws IOException {
String parentDirectory = getParentDirectory(name);
if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) {
writeParentDirectoriesIfNecessary(parentDirectory, time);
ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/');
entry.setUnixMode(UnixStat.DIR_FLAG);
entry.setTime(time);
this.out.putArchiveEntry(entry);
this.out.closeArchiveEntry();
}
}
private String getParentDirectory(String name) {
int lastSlash = name.lastIndexOf('/');
if (lastSlash == -1) {
return null;
}
return name.substring(0, lastSlash);
}
private String getEntryName(FileCopyDetails details) {
if (BootZipCopyAction.this.layerResolver == null) {
return details.getRelativePath().getPathString();
}
return BootZipCopyAction.this.layerResolver.getPath(details);
}
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
archiveEntry.setCrc(getCrc(details));
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
}
}
private long getCrc(FileCopyDetails details) {
Crc32OutputStream crcStream = new Crc32OutputStream();
details.copyTo(crcStream);
return crcStream.getCrc();
}
void finish() throws IOException { void finish() throws IOException {
writeLoaderEntriesIfNecessary(null); writeLoaderEntriesIfNecessary(null);
writeJarToolsIfNecessary();
writeLayersIndexIfNecessary();
writeClassPathIndexIfNecessary();
} }
private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException { private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException {
@ -183,9 +299,9 @@ class BootZipCopyAction implements CopyAction {
// Don't write loader entries until after META-INF folder (see gh-16698) // Don't write loader entries until after META-INF folder (see gh-16698)
return; return;
} }
LoaderZipEntries loaderEntries = new LoaderZipEntries( LoaderZipEntries entries = new LoaderZipEntries(
BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES); BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES);
this.writtenLoaderEntries = loaderEntries.writeTo(this.outputStream); this.writtenLoaderEntries = entries.writeTo(this.out);
} }
private boolean isInMetaInf(FileCopyDetails details) { private boolean isInMetaInf(FileCopyDetails details) {
@ -196,40 +312,47 @@ class BootZipCopyAction implements CopyAction {
return segments.length > 0 && "META-INF".equals(segments[0]); return segments.length > 0 && "META-INF".equals(segments[0]);
} }
private void processDirectory(FileCopyDetails details) throws IOException { private void writeJarToolsIfNecessary() throws IOException {
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/'); if (BootZipCopyAction.this.layerResolver == null || !BootZipCopyAction.this.includeLayerTools) {
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode()); return;
archiveEntry.setTime(getTime(details)); }
this.outputStream.putArchiveEntry(archiveEntry); writeJarModeLibrary(JarModeLibrary.LAYER_TOOLS);
this.outputStream.closeArchiveEntry();
} }
private void processFile(FileCopyDetails details) throws IOException { private void writeJarModeLibrary(JarModeLibrary jarModeLibrary) throws IOException {
String relativePath = details.getRelativePath().getPathString(); String name = BootZipCopyAction.this.layerResolver.getPath(jarModeLibrary);
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath); writeFile(name, ZipEntryWriter.fromInputStream(jarModeLibrary.openStream()));
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode()); }
archiveEntry.setTime(getTime(details));
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); private void writeLayersIndexIfNecessary() throws IOException {
if (compression == ZipCompression.STORED) { Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
prepareStoredEntry(details, archiveEntry); String layersIndex = (String) manifestAttributes.get("Spring-Boot-Layers-Index");
if (layersIndex != null && BootZipCopyAction.this.layerResolver != null) {
writeFile(layersIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding,
BootZipCopyAction.this.layerResolver.getLayerNames()));
} }
this.outputStream.putArchiveEntry(archiveEntry);
details.copyTo(this.outputStream);
this.outputStream.closeArchiveEntry();
} }
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { private void writeClassPathIndexIfNecessary() throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
archiveEntry.setSize(details.getSize()); String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
archiveEntry.setCompressedSize(details.getSize()); if (classPathIndex != null) {
Crc32OutputStream crcStream = new Crc32OutputStream(); writeFile(classPathIndex,
details.copyTo(crcStream); ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, this.writtenLibraries));
archiveEntry.setCrc(crcStream.getCrc());
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
} }
} }
private void writeFile(String name, ZipEntryWriter entryWriter) throws IOException {
writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES);
ZipArchiveEntry entry = new ZipArchiveEntry(name);
entry.setUnixMode(UnixStat.FILE_FLAG);
entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
this.out.putArchiveEntry(entry);
entryWriter.writeTo(entry, this.out);
this.out.closeArchiveEntry();
}
private long getTime(FileCopyDetails details) { private long getTime(FileCopyDetails details) {
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified() return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified()
: CONSTANT_TIME_FOR_ZIP_ENTRIES; : CONSTANT_TIME_FOR_ZIP_ENTRIES;
@ -237,6 +360,52 @@ class BootZipCopyAction implements CopyAction {
} }
/**
* Callback used to write a zip entry data.
*/
@FunctionalInterface
private interface ZipEntryWriter {
/**
* Write the entry data.
* @param entry the entry being written
* @param out the output stream used to write the data
* @throws IOException on IO error
*/
void writeTo(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException;
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* {@link InputStream}.
* @param in the source input stream
* @return a new {@link ZipEntryWriter} instance
*/
static ZipEntryWriter fromInputStream(InputStream in) {
return (entry, out) -> {
StreamUtils.copy(in, out);
in.close();
};
}
/**
* Create a new {@link ZipEntryWriter} that will copy content from the given
* lines.
* @param encoding the required character encoding
* @param lines the lines to write
* @return a new {@link ZipEntryWriter} instance
*/
static ZipEntryWriter fromLines(String encoding, Collection<String> lines) {
return (entry, out) -> {
OutputStreamWriter writer = new OutputStreamWriter(out, encoding);
for (String line : lines) {
writer.append(line + "\n");
}
writer.flush();
};
}
}
/** /**
* An {@code OutputStream} that provides a CRC-32 of the data that is written to it. * An {@code OutputStream} that provides a CRC-32 of the data that is written to it.
*/ */

@ -1,237 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.gradle.tasks.bundling;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.gradle.api.Action;
import org.gradle.api.tasks.Input;
import org.springframework.boot.loader.tools.layer.application.FilteredResourceStrategy;
import org.springframework.boot.loader.tools.layer.application.LocationFilter;
import org.springframework.boot.loader.tools.layer.application.ResourceFilter;
import org.springframework.boot.loader.tools.layer.application.ResourceStrategy;
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
import org.springframework.boot.loader.tools.layer.library.LibraryFilter;
import org.springframework.boot.loader.tools.layer.library.LibraryStrategy;
import org.springframework.util.Assert;
/**
* Encapsulates the configuration for a layered jar.
*
* @author Madhura Bhave
* @author Scott Frederick
* @since 2.3.0
*/
public class LayerConfiguration {
private boolean includeLayerTools = true;
private List<String> layersOrder = new ArrayList<>();
private List<ResourceStrategy> resourceStrategies = new ArrayList<>();
private List<LibraryStrategy> libraryStrategies = new ArrayList<>();
private StrategySpec strategySpec;
/**
* Whether to include the layer tools jar.
* @return true if layer tools is included
*/
@Input
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
public void setIncludeLayerTools(boolean includeLayerTools) {
this.includeLayerTools = includeLayerTools;
}
@Input
public List<String> getLayersOrder() {
return this.layersOrder;
}
public void layersOrder(String... layers) {
this.layersOrder = Arrays.asList(layers);
}
public void layersOrder(List<String> layers) {
this.layersOrder = layers;
}
@Input
public List<ResourceStrategy> getApplication() {
return this.resourceStrategies;
}
public void application(ResourceStrategy... resourceStrategies) {
assertLayersOrderConfigured();
this.resourceStrategies = Arrays.asList(resourceStrategies);
}
public void application(Action<LayerConfiguration> config) {
assertLayersOrderConfigured();
this.strategySpec = StrategySpec.forResources();
config.execute(this);
}
@Input
public List<LibraryStrategy> getLibraries() {
return this.libraryStrategies;
}
public void libraries(LibraryStrategy... strategies) {
assertLayersOrderConfigured();
this.libraryStrategies = Arrays.asList(strategies);
}
public void libraries(Action<LayerConfiguration> configure) {
assertLayersOrderConfigured();
this.strategySpec = StrategySpec.forLibraries();
configure.execute(this);
}
private void assertLayersOrderConfigured() {
Assert.state(!this.layersOrder.isEmpty(), "'layersOrder' must be configured before filters can be applied.");
}
public void layerContent(String layerName, Action<LayerConfiguration> config) {
this.strategySpec.newStrategy();
config.execute(this);
if (this.strategySpec.isLibrariesStrategy()) {
this.libraryStrategies.add(new FilteredLibraryStrategy(layerName, this.strategySpec.libraryFilters()));
}
else {
this.resourceStrategies.add(new FilteredResourceStrategy(layerName, this.strategySpec.resourceFilters()));
}
}
public void coordinates(Action<LayerConfiguration> config) {
Assert.state(this.strategySpec.isLibrariesStrategy(),
"The 'coordinates' filter must be used only with libraries");
this.strategySpec.newFilter();
config.execute(this);
this.strategySpec
.addLibraryFilter(new CoordinateFilter(this.strategySpec.includes(), this.strategySpec.excludes()));
}
public void locations(Action<LayerConfiguration> config) {
Assert.state(this.strategySpec.isResourcesStrategy(),
"The 'locations' filter must be used only with application");
this.strategySpec.newFilter();
config.execute(this);
this.strategySpec
.addResourceFilter(new LocationFilter(this.strategySpec.includes(), this.strategySpec.excludes()));
}
public void include(String... includes) {
this.strategySpec.include(includes);
}
public void exclude(String... excludes) {
this.strategySpec.exclude(excludes);
}
private static final class StrategySpec {
private final TYPE type;
private List<LibraryFilter> libraryFilters;
private List<ResourceFilter> resourceFilters;
private List<String> filterIncludes;
private List<String> filterExcludes;
private StrategySpec(TYPE type) {
this.type = type;
}
private boolean isLibrariesStrategy() {
return this.type == TYPE.LIBRARIES;
}
private boolean isResourcesStrategy() {
return this.type == TYPE.RESOURCES;
}
private void newStrategy() {
this.libraryFilters = new ArrayList<>();
this.resourceFilters = new ArrayList<>();
newFilter();
}
private void newFilter() {
this.filterIncludes = new ArrayList<>();
this.filterExcludes = new ArrayList<>();
}
private List<LibraryFilter> libraryFilters() {
return this.libraryFilters;
}
private void addLibraryFilter(LibraryFilter filter) {
this.libraryFilters.add(filter);
}
private List<ResourceFilter> resourceFilters() {
return this.resourceFilters;
}
private void addResourceFilter(ResourceFilter filter) {
this.resourceFilters.add(filter);
}
private List<String> includes() {
return this.filterIncludes;
}
private void include(String... includes) {
this.filterIncludes.addAll(Arrays.asList(includes));
}
private void exclude(String... excludes) {
this.filterIncludes.addAll(Arrays.asList(excludes));
}
private List<String> excludes() {
return this.filterExcludes;
}
private static StrategySpec forLibraries() {
return new StrategySpec(TYPE.LIBRARIES);
}
private static StrategySpec forResources() {
return new StrategySpec(TYPE.RESOURCES);
}
private enum TYPE {
LIBRARIES, RESOURCES;
}
}
}

@ -0,0 +1,197 @@
/*
* 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.gradle.tasks.bundling;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.specs.Spec;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Layers;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
/**
* Resolver backed by a {@link LayeredSpec} that provides the destination {@link Layer}
* for each copied {@link FileCopyDetails}.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @see BootZipCopyAction
*/
class LayerResolver {
private static final String BOOT_INF_FOLDER = "BOOT-INF/";
private final ResolvedDependencies resolvedDependencies;
private final LayeredSpec layeredConfiguration;
private final Spec<FileCopyDetails> librarySpec;
LayerResolver(Iterable<Configuration> configurations, LayeredSpec layeredConfiguration,
Spec<FileCopyDetails> librarySpec) {
this.resolvedDependencies = new ResolvedDependencies(configurations);
this.layeredConfiguration = layeredConfiguration;
this.librarySpec = librarySpec;
}
String getPath(JarModeLibrary jarModeLibrary) {
Layers layers = this.layeredConfiguration.asLayers();
Layer layer = layers.getLayer(jarModeLibrary);
if (layer != null) {
return BOOT_INF_FOLDER + "layers/" + layer + "/lib/" + jarModeLibrary.getName();
}
return BOOT_INF_FOLDER + "lib/" + jarModeLibrary.getName();
}
String getPath(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
Layer layer = getLayer(details);
if (layer == null || !path.startsWith(BOOT_INF_FOLDER)) {
return path;
}
path = path.substring(BOOT_INF_FOLDER.length());
return BOOT_INF_FOLDER + "layers/" + layer + "/" + path;
}
Layer getLayer(FileCopyDetails details) {
Layers layers = this.layeredConfiguration.asLayers();
try {
if (this.librarySpec.isSatisfiedBy(details)) {
return layers.getLayer(asLibrary(details));
}
return layers.getLayer(details.getSourcePath());
}
catch (UnsupportedOperationException ex) {
return null;
}
}
List<String> getLayerNames() {
return this.layeredConfiguration.asLayers().stream().map(Layer::toString).collect(Collectors.toList());
}
private Library asLibrary(FileCopyDetails details) {
File file = details.getFile();
LibraryCoordinates coordinates = this.resolvedDependencies.find(file);
return new Library(null, file, null, coordinates, false);
}
/**
* Tracks and provides details of resolved dependencies in the project so we can find
* {@link LibraryCoordinates}.
*/
private static class ResolvedDependencies {
private final Map<Configuration, ResolvedConfigurationDependencies> configurationDependencies = new LinkedHashMap<>();
ResolvedDependencies(Iterable<Configuration> configurations) {
configurations.forEach(this::processConfiguration);
}
private void processConfiguration(Configuration configuration) {
if (configuration.isCanBeResolved()) {
this.configurationDependencies.put(configuration,
new ResolvedConfigurationDependencies(configuration.getIncoming().getArtifacts()));
}
}
LibraryCoordinates find(File file) {
for (ResolvedConfigurationDependencies dependencies : this.configurationDependencies.values()) {
LibraryCoordinates coordinates = dependencies.find(file);
if (coordinates != null) {
return coordinates;
}
}
return null;
}
}
/**
* Stores details of resolved configuration dependencies.
*/
private static class ResolvedConfigurationDependencies {
private final Map<File, LibraryCoordinates> artifactCoordinates = new LinkedHashMap<>();
ResolvedConfigurationDependencies(ArtifactCollection resolvedDependencies) {
if (resolvedDependencies != null) {
for (ResolvedArtifactResult resolvedArtifact : resolvedDependencies.getArtifacts()) {
ComponentIdentifier identifier = resolvedArtifact.getId().getComponentIdentifier();
if (identifier instanceof ModuleComponentIdentifier) {
this.artifactCoordinates.put(resolvedArtifact.getFile(),
new ModuleComponentIdentifierLibraryCoordinates(
(ModuleComponentIdentifier) identifier));
}
}
}
}
LibraryCoordinates find(File file) {
return this.artifactCoordinates.get(file);
}
}
/**
* Adapts a {@link ModuleComponentIdentifier} to {@link LibraryCoordinates}.
*/
private static class ModuleComponentIdentifierLibraryCoordinates implements LibraryCoordinates {
private final ModuleComponentIdentifier identifier;
ModuleComponentIdentifierLibraryCoordinates(ModuleComponentIdentifier identifier) {
this.identifier = identifier;
}
@Override
public String getGroupId() {
return this.identifier.getGroup();
}
@Override
public String getArtifactId() {
return this.identifier.getModule();
}
@Override
public String getVersion() {
return this.identifier.getVersion();
}
@Override
public String toString() {
return this.identifier.toString();
}
}
}

@ -0,0 +1,228 @@
/*
* 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.gradle.tasks.bundling;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.util.ConfigureUtil;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Layers;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.layer.ApplicationContentFilter;
import org.springframework.boot.loader.tools.layer.ContentFilter;
import org.springframework.boot.loader.tools.layer.ContentSelector;
import org.springframework.boot.loader.tools.layer.CustomLayers;
import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector;
import org.springframework.boot.loader.tools.layer.LibraryContentFilter;
import org.springframework.util.Assert;
/**
* Encapsulates the configuration for a layered jar.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @since 2.3.0
*/
public class LayeredSpec {
private boolean includeLayerTools = true;
private ApplicationSpec application = new ApplicationSpec();
private DependenciesSpec dependencies = new DependenciesSpec();
@Optional
private List<String> layerOrder;
private Layers layers;
@Input
public boolean isIncludeLayerTools() {
return this.includeLayerTools;
}
public void setIncludeLayerTools(boolean includeLayerTools) {
this.includeLayerTools = includeLayerTools;
}
@Input
public ApplicationSpec getApplication() {
return this.application;
}
public void application(ApplicationSpec spec) {
this.application = spec;
}
public void application(Closure<?> closure) {
application(ConfigureUtil.configureUsing(closure));
}
public void application(Action<ApplicationSpec> action) {
action.execute(this.application);
}
@Input
public DependenciesSpec getDependencies() {
return this.dependencies;
}
public void dependencies(DependenciesSpec spec) {
this.dependencies = spec;
}
public void dependencies(Closure<?> closure) {
dependencies(ConfigureUtil.configureUsing(closure));
}
public void dependencies(Action<DependenciesSpec> action) {
action.execute(this.dependencies);
}
@Input
public List<String> getLayerOrder() {
return this.layerOrder;
}
public void layerOrder(String... layerOrder) {
this.layerOrder = Arrays.asList(layerOrder);
}
public void layerOrder(List<String> layerOrder) {
this.layerOrder = layerOrder;
}
/**
* Return this configuration as a {@link Layers} instance. This method should only be
* called when the configuration is complete and will no longer be changed.
* @return the layers
*/
Layers asLayers() {
Layers layers = this.layers;
if (layers == null) {
layers = createLayers();
this.layers = layers;
}
return layers;
}
private Layers createLayers() {
if (this.layerOrder == null || this.layerOrder.isEmpty()) {
Assert.state(this.application.isEmpty() && this.dependencies.isEmpty(),
"The 'layerOrder' must be defined when using custom layering");
return Layers.IMPLICIT;
}
List<Layer> layers = this.layerOrder.stream().map(Layer::new).collect(Collectors.toList());
return new CustomLayers(layers, this.application.asSelectors(), this.dependencies.asSelectors());
}
public abstract static class IntoLayersSpec implements Serializable {
private final List<IntoLayerSpec> intoLayers;
boolean isEmpty() {
return this.intoLayers.isEmpty();
}
IntoLayersSpec(IntoLayerSpec... spec) {
this.intoLayers = new ArrayList<>(Arrays.asList(spec));
}
public void intoLayer(String layer) {
this.intoLayers.add(new IntoLayerSpec(layer));
}
public void intoLayer(String layer, Closure<?> closure) {
intoLayer(layer, ConfigureUtil.configureUsing(closure));
}
public void intoLayer(String layer, Action<IntoLayerSpec> action) {
IntoLayerSpec spec = new IntoLayerSpec(layer);
action.execute(spec);
this.intoLayers.add(spec);
}
<T> List<ContentSelector<T>> asSelectors(Function<String, ContentFilter<T>> filterFactory) {
return this.intoLayers.stream().map((content) -> content.asSelector(filterFactory))
.collect(Collectors.toList());
}
}
public static class IntoLayerSpec implements Serializable {
private final String intoLayer;
private final List<String> includes = new ArrayList<>();
private final List<String> excludes = new ArrayList<>();
public IntoLayerSpec(String intoLayer) {
this.intoLayer = intoLayer;
}
public void include(String... patterns) {
this.includes.addAll(Arrays.asList(patterns));
}
public void exclude(String... patterns) {
this.includes.addAll(Arrays.asList(patterns));
}
<T> ContentSelector<T> asSelector(Function<String, ContentFilter<T>> filterFactory) {
Layer layer = new Layer(this.intoLayer);
return new IncludeExcludeContentSelector<>(layer, this.includes, this.excludes, filterFactory);
}
}
public static class ApplicationSpec extends IntoLayersSpec {
public ApplicationSpec(IntoLayerSpec... contents) {
super(contents);
}
List<ContentSelector<String>> asSelectors() {
return asSelectors(ApplicationContentFilter::new);
}
}
public static class DependenciesSpec extends IntoLayersSpec {
public DependenciesSpec(IntoLayerSpec... contents) {
super(contents);
}
List<ContentSelector<Library>> asSelectors() {
return asSelectors(LibraryContentFilter::new);
}
}
}

@ -44,18 +44,18 @@ class LoaderZipEntries {
this.entryTime = entryTime; this.entryTime = entryTime;
} }
Spec<FileTreeElement> writeTo(ZipArchiveOutputStream zipOutputStream) throws IOException { Spec<FileTreeElement> writeTo(ZipArchiveOutputStream out) throws IOException {
WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec(); WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec();
try (ZipInputStream loaderJar = new ZipInputStream( try (ZipInputStream loaderJar = new ZipInputStream(
getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); java.util.zip.ZipEntry entry = loaderJar.getNextEntry();
while (entry != null) { while (entry != null) {
if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) {
writeDirectory(new ZipArchiveEntry(entry), zipOutputStream); writeDirectory(new ZipArchiveEntry(entry), out);
writtenDirectoriesSpec.add(entry); writtenDirectoriesSpec.add(entry);
} }
else if (entry.getName().endsWith(".class")) { else if (entry.getName().endsWith(".class")) {
writeClass(new ZipArchiveEntry(entry), loaderJar, zipOutputStream); writeClass(new ZipArchiveEntry(entry), loaderJar, out);
} }
entry = loaderJar.getNextEntry(); entry = loaderJar.getNextEntry();
} }

@ -25,11 +25,14 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException; import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome; import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
@ -75,8 +78,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
writeResource(); writeResource();
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar")) assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull();
.isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
.isNotNull(); .isNotNull();
@ -89,10 +91,11 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
void customLayers() throws IOException { void customLayers() throws IOException {
writeMainClass(); writeMainClass();
writeResource(); writeResource();
assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); BuildResult build = this.gradleBuild.build("bootJar");
System.out.println(build.getOutput());
assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar")) assertThat(jarFile.getEntry(jarModeLayerTools())).isNotNull();
.isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry("BOOT-INF/layers/commons-dependencies/lib/commons-lang3-3.9.jar")).isNotNull();
assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar")) assertThat(jarFile.getEntry("BOOT-INF/layers/snapshot-dependencies/lib/commons-io-2.7-SNAPSHOT.jar"))
.isNotNull(); .isNotNull();
@ -101,6 +104,13 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
} }
} }
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
}
private void writeMainClass() { private void writeMainClass() {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs(); examplePackage.mkdirs();

@ -20,9 +20,8 @@ import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.jar.JarFile; import java.util.jar.JarFile;
@ -31,16 +30,15 @@ import java.util.zip.ZipEntry;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.artifacts.ArtifactCollection; import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.artifacts.ResolvableDependencies;
import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; import org.gradle.api.artifacts.component.ComponentArtifactIdentifier;
import org.gradle.api.artifacts.component.ComponentIdentifier; import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.layer.application.FilteredResourceStrategy; import org.springframework.boot.gradle.tasks.bundling.BootJarTests.TestBootJar;
import org.springframework.boot.loader.tools.layer.application.LocationFilter; import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -53,10 +51,10 @@ import static org.mockito.Mockito.mock;
* @author Madhura Bhave * @author Madhura Bhave
* @author Scott Frederick * @author Scott Frederick
*/ */
class BootJarTests extends AbstractBootArchiveTests<BootJar> { class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
BootJarTests() { BootJarTests() {
super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/"); super(TestBootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/");
} }
@Test @Test
@ -121,13 +119,17 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test @Test
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException { void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
File jar = createLayeredJar((configuration) -> { File jar = createLayeredJar((layered) -> {
configuration.layersOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); layered.application((application) -> {
configuration.libraries(createLibraryStrategy("my-snapshot-deps", "com.example:*:*.SNAPSHOT"), application.intoLayer("resources", (spec) -> spec.include("static/**"));
createLibraryStrategy("my-internal-deps", "com.example:*:*"), application.intoLayer("application");
createLibraryStrategy("my-deps", "*:*")); });
configuration.application(createResourceStrategy("resources", "static/**"), layered.dependencies((dependencies) -> {
createResourceStrategy("application", "**")); dependencies.intoLayer("my-snapshot-deps", (spec) -> spec.include("com.example:*:*.SNAPSHOT"));
dependencies.intoLayer("my-internal-deps", (spec) -> spec.include("com.example:*:*"));
dependencies.intoLayer("my-deps");
});
layered.layerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
}); });
List<String> entryNames = getEntryNames(jar); List<String> entryNames = getEntryNames(jar);
assertThat(entryNames) assertThat(entryNames)
@ -139,16 +141,6 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
.contains("BOOT-INF/layers/resources/classes/static/test.css"); .contains("BOOT-INF/layers/resources/classes/static/test.css");
} }
private FilteredLibraryStrategy createLibraryStrategy(String layerName, String... includes) {
return new FilteredLibraryStrategy(layerName,
Collections.singletonList(new CoordinateFilter(Arrays.asList(includes), Collections.emptyList())));
}
private FilteredResourceStrategy createResourceStrategy(String layerName, String... includes) {
return new FilteredResourceStrategy(layerName,
Collections.singletonList(new LocationFilter(Arrays.asList(includes), Collections.emptyList())));
}
@Test @Test
void whenJarIsLayeredJarsInLibAreStored() throws IOException { void whenJarIsLayeredJarsInLibAreStored() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) { try (JarFile jarFile = new JarFile(createLayeredJar())) {
@ -172,7 +164,14 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test @Test
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar()); List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); assertThat(entryNames).contains(jarModeLayerTools());
}
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
} }
@Test @Test
@ -198,24 +197,24 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
return getTask().getArchiveFile().get().getAsFile(); return getTask().getArchiveFile().get().getAsFile();
} }
private File createLayeredJar(Action<LayerConfiguration> action) throws IOException { private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private File createLayeredJar(Action<LayeredSpec> action) throws IOException {
if (action != null) { if (action != null) {
getTask().layers(action); getTask().layered(action);
} }
else { else {
getTask().layers(); getTask().layered();
} }
addContent(); addContent();
executeTask(); executeTask();
return getTask().getArchiveFile().get().getAsFile(); return getTask().getArchiveFile().get().getAsFile();
} }
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private void addContent() throws IOException { private void addContent() throws IOException {
BootJar bootJar = getTask(); TestBootJar bootJar = getTask();
bootJar.setMainClassName("com.example.Main"); bootJar.setMainClassName("com.example.Main");
File classesJavaMain = new File(this.temp, "classes/java/main"); File classesJavaMain = new File(this.temp, "classes/java/main");
File applicationClass = new File(classesJavaMain, "com/example/Application.class"); File applicationClass = new File(classesJavaMain, "com/example/Application.class");
@ -231,32 +230,33 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
css.createNewFile(); css.createNewFile();
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
jarFile("third-library-SNAPSHOT.jar")); jarFile("third-library-SNAPSHOT.jar"));
Set<ResolvedArtifactResult> artifacts = new LinkedHashSet<>();
Set<ResolvedArtifactResult> resolvedArtifacts = new HashSet<>(); artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0"));
resolvedArtifacts.add(mockLibraryArtifact("first-library.jar", "com.example:first-library:1.0.0")); artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0"));
resolvedArtifacts.add(mockLibraryArtifact("second-library.jar", "com.example:second-library:1.0.0")); artifacts.add(
resolvedArtifacts mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
.add(mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example:third-library:1.0.0.SNAPSHOT")); ArtifactCollection resolvedDependencies = mock(ArtifactCollection.class);
given(resolvedDependencies.getArtifacts()).willReturn(artifacts);
ArtifactCollection artifacts = mock(ArtifactCollection.class); ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class);
given(artifacts.getArtifacts()).willReturn(resolvedArtifacts); given(resolvableDependencies.getArtifacts()).willReturn(resolvedDependencies);
Configuration configuration = mock(Configuration.class);
ResolvableDependencies deps = mock(ResolvableDependencies.class); given(configuration.isCanBeResolved()).willReturn(true);
given(deps.getArtifacts()).willReturn(artifacts); given(configuration.getIncoming()).willReturn(resolvableDependencies);
bootJar.resolvedDependencies(deps); bootJar.setConfiguration(Collections.singleton(configuration));
} }
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String coordinates) { private ResolvedArtifactResult mockLibraryArtifact(String fileName, String group, String module, String version) {
ComponentIdentifier libraryId = mock(ComponentIdentifier.class); ModuleComponentIdentifier identifier = mock(ModuleComponentIdentifier.class);
given(libraryId.getDisplayName()).willReturn(coordinates); given(identifier.getGroup()).willReturn(group);
given(identifier.getModule()).willReturn(module);
given(identifier.getVersion()).willReturn(version);
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class); ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
given(libraryArtifactId.getComponentIdentifier()).willReturn(libraryId); given(libraryArtifactId.getComponentIdentifier()).willReturn(identifier);
ResolvedArtifactResult libraryArtifact = mock(ResolvedArtifactResult.class); ResolvedArtifactResult libraryArtifact = mock(ResolvedArtifactResult.class);
given(libraryArtifact.getFile()).willReturn(new File(fileName)); File file = new File(this.temp, fileName).getAbsoluteFile();
System.out.println(file);
given(libraryArtifact.getFile()).willReturn(file);
given(libraryArtifact.getId()).willReturn(libraryArtifactId); given(libraryArtifact.getId()).willReturn(libraryArtifactId);
return libraryArtifact; return libraryArtifact;
} }
@ -272,4 +272,19 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
getTask().copy(); getTask().copy();
} }
public static class TestBootJar extends BootJar {
private Iterable<Configuration> configurations = Collections.emptySet();
@Override
protected Iterable<Configuration> getConfigurations() {
return this.configurations;
}
void setConfiguration(Iterable<Configuration> configurations) {
this.configurations = configurations;
}
}
} }

@ -5,26 +5,23 @@ plugins {
bootJar { bootJar {
mainClassName = 'com.example.Application' mainClassName = 'com.example.Application'
layers { layered {
layersOrder "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"
libraries {
layerContent("snapshot-dependencies") { coordinates { include "*:*:*SNAPSHOT" } }
layerContent("commons-dependencies") { coordinates { include "org.apache.commons:*" } }
layerContent("dependencies") { coordinates { include "*:*" } }
}
application { application {
layerContent("static") { intoLayer("static") {
locations { include "META-INF/resources/**", "resources/**", "static/**", "public/**"
include "META-INF/resources/**", "resources/**" }
include "static/**", "public/**" intoLayer("app")
} }
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
} }
layerContent("app") { intoLayer("commons-dependencies") {
locations { include "org.apache.commons:*"
include "**"
}
} }
intoLayer("dependencies")
} }
layerOrder "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"
} }
} }

@ -5,7 +5,7 @@ plugins {
bootJar { bootJar {
mainClassName = 'com.example.Application' mainClassName = 'com.example.Application'
layers() layered()
} }
repositories { repositories {

@ -11,7 +11,7 @@ bootJar {
} }
} }
if (project.hasProperty('layered') && project.getProperty('layered')) { if (project.hasProperty('layered') && project.getProperty('layered')) {
layers { layered {
includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true
} }
} }

@ -0,0 +1,79 @@
/*
* 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.loader.tools;
/**
* Encapsulates information about the artifact coordinates of a library.
*
* @author Scott Frederick
*/
class DefaultLibraryCoordinates implements LibraryCoordinates {
private final String groupId;
private final String artifactId;
private final String version;
/**
* Create a new instance from discrete elements.
* @param groupId the group ID
* @param artifactId the artifact ID
* @param version the version
*/
DefaultLibraryCoordinates(String groupId, String artifactId, String version) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
}
/**
* Return the group ID of the coordinates.
* @return the group ID
*/
@Override
public String getGroupId() {
return this.groupId;
}
/**
* Return the artifact ID of the coordinates.
* @return the artifact ID
*/
@Override
public String getArtifactId() {
return this.artifactId;
}
/**
* Return the version of the coordinates.
* @return the version
*/
@Override
public String getVersion() {
return this.version;
}
/**
* Return the coordinates in the form {@code groupId:artifactId:version}.
*/
@Override
public String toString() {
return LibraryCoordinates.toStandardNotationString(this);
}
}

@ -22,23 +22,48 @@ import java.io.InputStream;
import java.net.URL; import java.net.URL;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* {@link Library} implementation for internal jarmode jars. * {@link Library} implementation for internal jarmode jars.
* *
* @author Phillip Webb * @author Phillip Webb
* @since 2.3.0
*/ */
class JarModeLibrary extends Library { public class JarModeLibrary extends Library {
static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools.jar"); /**
* {@link JarModeLibrary} for layer tools.
*/
public static final JarModeLibrary LAYER_TOOLS = new JarModeLibrary("spring-boot-jarmode-layertools");
JarModeLibrary(String name) { JarModeLibrary(String artifactId) {
super(name, null, LibraryScope.RUNTIME, false); this(createCoordinates(artifactId));
}
public JarModeLibrary(LibraryCoordinates coordinates) {
super(getJarName(coordinates), null, LibraryScope.RUNTIME, coordinates, false);
}
private static LibraryCoordinates createCoordinates(String artifactId) {
String version = JarModeLibrary.class.getPackage().getImplementationVersion();
return LibraryCoordinates.of("org.springframework.boot", artifactId, version);
}
private static String getJarName(LibraryCoordinates coordinates) {
String version = coordinates.getVersion();
StringBuilder jarName = new StringBuilder(coordinates.getArtifactId());
if (StringUtils.hasText(version)) {
jarName.append('-');
jarName.append(version);
}
jarName.append(".jar");
return jarName.toString();
} }
@Override @Override
InputStream openStream() throws IOException { public InputStream openStream() throws IOException {
String path = "META-INF/jarmode/" + getName(); String path = "META-INF/jarmode/" + getCoordinates().getArtifactId() + ".jar";
URL resource = getClass().getClassLoader().getResource(path); URL resource = getClass().getClassLoader().getResource(path);
Assert.state(resource != null, "Unable to find resource " + path); Assert.state(resource != null, "Unable to find resource " + path);
return resource.openStream(); return resource.openStream();

@ -16,7 +16,6 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.Serializable;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -29,7 +28,7 @@ import org.springframework.util.Assert;
* @since 2.3.0 * @since 2.3.0
* @see Layers * @see Layers
*/ */
public class Layer implements Serializable { public class Layer {
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$"); private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");

@ -17,6 +17,7 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.util.Iterator; import java.util.Iterator;
import java.util.stream.Stream;
/** /**
* Interface to provide information about layers to the {@link Repackager}. * Interface to provide information about layers to the {@link Repackager}.
@ -36,16 +37,25 @@ public interface Layers extends Iterable<Layer> {
/** /**
* Return the jar layers in the order that they should be added (starting with the * Return the jar layers in the order that they should be added (starting with the
* least frequently changed layer). * least frequently changed layer).
* @return the layers iterator
*/ */
@Override @Override
Iterator<Layer> iterator(); Iterator<Layer> iterator();
/**
* Return a stream of the jar layers in the order that they should be added (starting
* with the least frequently changed layer).
* @return the layers stream
*/
Stream<Layer> stream();
/** /**
* Return the layer that contains the given resource name. * Return the layer that contains the given resource name.
* @param resourceName the name of the resource (for example a {@code .class} file). * @param applicationResource the name of an application resource (for example a
* {@code .class} file).
* @return the layer that contains the resource (must never be {@code null}) * @return the layer that contains the resource (must never be {@code null})
*/ */
Layer getLayer(String resourceName); Layer getLayer(String applicationResource);
/** /**
* Return the layer that contains the given library. * Return the layer that contains the given library.

@ -16,82 +16,60 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import org.springframework.util.Assert;
/** /**
* Encapsulates information about the Maven artifact coordinates of a library. * Encapsulates information about the artifact coordinates of a library.
* *
* @author Scott Frederick * @author Scott Frederick
* @author Phillip Webb
* @since 2.3.0 * @since 2.3.0
*/ */
public final class LibraryCoordinates { public interface LibraryCoordinates {
private final String groupId;
private final String artifactId;
private final String version;
/**
* Create a new instance from discrete elements.
* @param groupId the group ID
* @param artifactId the artifact ID
* @param version the version
*/
public LibraryCoordinates(String groupId, String artifactId, String version) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
}
/**
* Create a new instance from a String value in the form
* {@code groupId:artifactId:version} where the version is optional.
* @param coordinates the coordinates
*/
public LibraryCoordinates(String coordinates) {
String[] elements = coordinates.split(":");
Assert.isTrue(elements.length >= 2, "Coordinates must contain at least 'groupId:artifactId'");
this.groupId = elements[0];
this.artifactId = elements[1];
this.version = (elements.length > 2) ? elements[2] : null;
}
/** /**
* Return the group ID of the coordinates. * Return the group ID of the coordinates.
* @return the group ID * @return the group ID
*/ */
public String getGroupId() { String getGroupId();
return this.groupId;
}
/** /**
* Return the artifact ID of the coordinates. * Return the artifact ID of the coordinates.
* @return the artifact ID * @return the artifact ID
*/ */
public String getArtifactId() { String getArtifactId();
return this.artifactId;
}
/** /**
* Return the version of the coordinates. * Return the version of the coordinates.
* @return the version * @return the version
*/ */
public String getVersion() { String getVersion();
return this.version;
/**
* Factory method to create {@link LibraryCoordinates} with the specified values.
* @param groupId the group ID
* @param artifactId the artifact ID
* @param version the version
* @return a new {@link LibraryCoordinates} instance
*/
static LibraryCoordinates of(String groupId, String artifactId, String version) {
return new DefaultLibraryCoordinates(groupId, artifactId, version);
} }
/** /**
* Return the coordinates in the form {@code groupId:artifactId:version}. * Utility method that returns the given coordinates using the standard
* {@code group:artifact:version} form.
* @param coordinates the coordinates to convert (may be {@code null})
* @return the standard notation form or {@code "::"} when the coordinates are null
*/ */
@Override static String toStandardNotationString(LibraryCoordinates coordinates) {
public String toString() { if (coordinates == null) {
return "::";
}
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append((this.groupId != null) ? this.groupId : ""); builder.append((coordinates.getGroupId() != null) ? coordinates.getGroupId() : "");
builder.append(":"); builder.append(":");
builder.append((this.artifactId != null) ? this.artifactId : ""); builder.append((coordinates.getArtifactId() != null) ? coordinates.getArtifactId() : "");
builder.append(":"); builder.append(":");
builder.append((this.version != null) ? this.version : ""); builder.append((coordinates.getVersion() != null) ? coordinates.getVersion() : "");
return builder.toString(); return builder.toString();
} }

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
/** /**
* Base class for the standard set of {@link Layers}. Defines the following layers: * Base class for the standard set of {@link Layers}. Defines the following layers:
@ -64,4 +65,9 @@ public abstract class StandardLayers implements Layers {
return LAYERS.iterator(); return LAYERS.iterator();
} }
@Override
public Stream<Layer> stream() {
return LAYERS.stream();
}
} }

@ -14,29 +14,33 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.loader.tools.layer.application; package org.springframework.boot.loader.tools.layer;
import java.util.List;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
/** /**
* An implementation of {@link ResourceFilter} based on the resource location. * {@link ContentFilter} that matches application items based on an Ant-style path
* pattern.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @author Phillip Webb
* @since 2.3.0 * @since 2.3.0
*/ */
public class LocationFilter extends AbstractResourceFilter { public class ApplicationContentFilter implements ContentFilter<String> {
private static final AntPathMatcher MATCHER = new AntPathMatcher(); private static final AntPathMatcher MATCHER = new AntPathMatcher();
public LocationFilter(List<String> includes, List<String> excludes) { private final String pattern;
super(includes, excludes);
public ApplicationContentFilter(String pattern) {
Assert.hasText(pattern, "Pattern must not be empty");
this.pattern = pattern;
} }
@Override @Override
protected boolean isMatch(String resourceName, List<String> toMatch) { public boolean matches(String path) {
return toMatch.stream().anyMatch((pattern) -> MATCHER.match(pattern, resourceName)); return MATCHER.match(this.pattern, path);
} }
} }

@ -14,8 +14,24 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.loader.tools.layer;
/** /**
* Support for custom layers for everything in BOOT-INF/classes. * Callback interface that can be used to filter layer contents.
* *
* @author Madhura Bhave
* @author Phillip Webb
* @param <T> the content type
* @since 2.3.0
*/ */
package org.springframework.boot.loader.tools.layer.application; @FunctionalInterface
public interface ContentFilter<T> {
/**
* Return if the filter matches the specified item.
* @param item the item to test
* @return if the filter matches
*/
boolean matches(T item);
}

@ -14,27 +14,32 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.loader.tools.layer.library; package org.springframework.boot.loader.tools.layer;
import java.io.Serializable;
import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Library;
/** /**
* A strategy used to match a library to a layer. * Strategy used by {@link CustomLayers} to select the layer of an item.
* *
* @param <T> the content type
* @author Madhura Bhave * @author Madhura Bhave
* @author Phillip Webb
* @since 2.3.0 * @since 2.3.0
* @see IncludeExcludeContentSelector
*/ */
public interface LibraryStrategy extends Serializable { public interface ContentSelector<T> {
/**
* Return the {@link Layer} that the selector represents.
* @return the named layer
*/
Layer getLayer();
/** /**
* Return a {@link Layer} for the given {@link Library}. If no matching layer is * Returns {@code true} if the specified item is contained in this selection.
* found, {@code null} is returned. * @param item the item to test
* @param library the library * @return if the item is contained
* @return the matching layer or {@code null}
*/ */
Layer getMatchingLayer(Library library); boolean contains(T item);
} }

@ -19,32 +19,52 @@ package org.springframework.boot.loader.tools.layer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Layers; import org.springframework.boot.loader.tools.Layers;
import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.layer.application.ResourceStrategy; import org.springframework.util.Assert;
import org.springframework.boot.loader.tools.layer.library.LibraryStrategy;
/** /**
* Implementation of {@link Layers} representing user-provided layers. * Custom {@link Layers} implementation where layer content is selected by the user.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @author Phillip Webb
* @since 2.3.0 * @since 2.3.0
*/ */
public class CustomLayers implements Layers { public class CustomLayers implements Layers {
private final List<Layer> layers; private final List<Layer> layers;
private final List<ResourceStrategy> resourceStrategies; private final List<ContentSelector<String>> applicationSelectors;
private final List<LibraryStrategy> libraryStrategies; private final List<ContentSelector<Library>> librarySelectors;
public CustomLayers(List<Layer> layers, List<ResourceStrategy> resourceStrategies, public CustomLayers(List<Layer> layers, List<ContentSelector<String>> applicationSelectors,
List<LibraryStrategy> libraryStrategies) { List<ContentSelector<Library>> librarySelectors) {
Assert.notNull(layers, "Layers must not be null");
Assert.notNull(applicationSelectors, "ApplicationSelectors must not be null");
validateSelectorLayers(applicationSelectors, layers);
Assert.notNull(librarySelectors, "LibrarySelectors must not be null");
validateSelectorLayers(librarySelectors, layers);
this.layers = new ArrayList<>(layers); this.layers = new ArrayList<>(layers);
this.resourceStrategies = new ArrayList<>(resourceStrategies); this.applicationSelectors = new ArrayList<>(applicationSelectors);
this.libraryStrategies = new ArrayList<>(libraryStrategies); this.librarySelectors = new ArrayList<>(librarySelectors);
}
private static <T> void validateSelectorLayers(List<ContentSelector<T>> selectors, List<Layer> layers) {
for (ContentSelector<?> selector : selectors) {
validateSelectorLayers(selector, layers);
}
}
private static void validateSelectorLayers(ContentSelector<?> selector, List<Layer> layers) {
Layer layer = selector.getLayer();
Assert.state(layer != null, "Missing content selector layer");
Assert.state(layers.contains(layer),
"Content selector layer '" + selector.getLayer() + "' not found in " + layers);
} }
@Override @Override
@ -52,35 +72,28 @@ public class CustomLayers implements Layers {
return this.layers.iterator(); return this.layers.iterator();
} }
@Override
public Stream<Layer> stream() {
return this.layers.stream();
}
@Override @Override
public Layer getLayer(String resourceName) { public Layer getLayer(String resourceName) {
for (ResourceStrategy strategy : this.resourceStrategies) { return selectLayer(resourceName, this.applicationSelectors, () -> "Resource '" + resourceName + "'");
Layer matchingLayer = strategy.getMatchingLayer(resourceName);
if (matchingLayer != null) {
validateLayerName(matchingLayer, "Resource '" + resourceName + "'");
return matchingLayer;
}
}
throw new IllegalStateException("Resource '" + resourceName + "' did not match any layer.");
} }
@Override @Override
public Layer getLayer(Library library) { public Layer getLayer(Library library) {
for (LibraryStrategy strategy : this.libraryStrategies) { return selectLayer(library, this.librarySelectors, () -> "Library '" + library.getName() + "'");
Layer matchingLayer = strategy.getMatchingLayer(library);
if (matchingLayer != null) {
validateLayerName(matchingLayer, "Library '" + library.getName() + "'");
return matchingLayer;
}
}
throw new IllegalStateException("Library '" + library.getName() + "' did not match any layer.");
} }
private void validateLayerName(Layer layer, String nameText) { private <T> Layer selectLayer(T item, List<ContentSelector<T>> selectors, Supplier<String> name) {
if (!this.layers.contains(layer)) { for (ContentSelector<T> selector : selectors) {
throw new IllegalStateException(nameText + " matched a layer '" + layer if (selector.contains(item)) {
+ "' that is not included in the configured layers " + this.layers + "."); return selector.getLayer();
}
} }
throw new IllegalStateException(name.get() + " did not match any layer");
} }
} }

@ -0,0 +1,96 @@
/*
* 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.loader.tools.layer;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.util.Assert;
/**
* {@link ContentSelector} backed by {@code include}/{@code exclude} {@link ContentFilter
* filters}.
*
* @param <T> the content type
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.3.0
*/
public class IncludeExcludeContentSelector<T> implements ContentSelector<T> {
private final Layer layer;
private final List<ContentFilter<T>> includes;
private final List<ContentFilter<T>> excludes;
public IncludeExcludeContentSelector(Layer layer, List<ContentFilter<T>> includes,
List<ContentFilter<T>> excludes) {
this(layer, includes, excludes, Function.identity());
}
public <S> IncludeExcludeContentSelector(Layer layer, List<S> includes, List<S> excludes,
Function<S, ContentFilter<T>> filterFactory) {
Assert.notNull(layer, "Layer must not be null");
Assert.notNull(filterFactory, "FilterFactory must not be null");
this.layer = layer;
this.includes = (includes != null) ? adapt(includes, filterFactory) : Collections.emptyList();
this.excludes = (excludes != null) ? adapt(excludes, filterFactory) : Collections.emptyList();
}
private <S> List<ContentFilter<T>> adapt(List<S> list, Function<S, ContentFilter<T>> mapper) {
return list.stream().map(mapper).collect(Collectors.toList());
}
@Override
public Layer getLayer() {
return this.layer;
}
@Override
public boolean contains(T item) {
return isIncluded(item) && !isExcluded(item);
}
private boolean isIncluded(T item) {
if (this.includes.isEmpty()) {
return true;
}
for (ContentFilter<T> include : this.includes) {
if (include.matches(item)) {
return true;
}
}
return false;
}
private boolean isExcluded(T item) {
if (this.excludes.isEmpty()) {
return false;
}
for (ContentFilter<T> exclude : this.excludes) {
if (exclude.matches(item)) {
return true;
}
}
return false;
}
}

@ -0,0 +1,61 @@
/*
* 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.loader.tools.layer;
import java.util.regex.Pattern;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.util.Assert;
/**
* {@link ContentFilter} that matches {@link Library} items based on a coordinates
* pattern.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @since 2.3.0
*/
public class LibraryContentFilter implements ContentFilter<Library> {
private final Pattern pattern;
public LibraryContentFilter(String coordinatesPattern) {
Assert.hasText(coordinatesPattern, "CoordinatesPattern must not be empty");
StringBuilder regex = new StringBuilder();
for (int i = 0; i < coordinatesPattern.length(); i++) {
char c = coordinatesPattern.charAt(i);
if (c == '.') {
regex.append("\\.");
}
else if (c == '*') {
regex.append(".*");
}
else {
regex.append(c);
}
}
this.pattern = Pattern.compile(regex.toString());
}
@Override
public boolean matches(Library library) {
return this.pattern.matcher(LibraryCoordinates.toStandardNotationString(library.getCoordinates())).matches();
}
}

@ -1,51 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract base class for {@link ResourceFilter} implementations.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public abstract class AbstractResourceFilter implements ResourceFilter {
private final List<String> includes = new ArrayList<>();
private final List<String> excludes = new ArrayList<>();
public AbstractResourceFilter(List<String> includes, List<String> excludes) {
this.includes.addAll(includes);
this.excludes.addAll(excludes);
}
@Override
public boolean isResourceIncluded(String resourceName) {
return isMatch(resourceName, this.includes);
}
@Override
public boolean isResourceExcluded(String resourceName) {
return isMatch(resourceName, this.excludes);
}
protected abstract boolean isMatch(String resourceName, List<String> toMatch);
}

@ -1,61 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.util.Assert;
/**
* A {@link ResourceStrategy} with custom filters.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class FilteredResourceStrategy implements ResourceStrategy {
private final Layer layer;
private final List<ResourceFilter> filters = new ArrayList<>();
public FilteredResourceStrategy(String layer, List<ResourceFilter> filters) {
Assert.notEmpty(filters, "Filters should not be empty for custom strategy.");
this.layer = new Layer(layer);
this.filters.addAll(filters);
}
public Layer getLayer() {
return this.layer;
}
@Override
public Layer getMatchingLayer(String resourceName) {
boolean isIncluded = false;
for (ResourceFilter filter : this.filters) {
if (filter.isResourceExcluded(resourceName)) {
return null;
}
if (!isIncluded && filter.isResourceIncluded(resourceName)) {
isIncluded = true;
}
}
return (isIncluded) ? this.layer : null;
}
}

@ -1,43 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.io.Serializable;
/**
* A filter that can tell if a resource has been included or excluded.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public interface ResourceFilter extends Serializable {
/**
* Return true if the resource is included by the filter.
* @param resourceName the resource name
* @return true if the resource is included
*/
boolean isResourceIncluded(String resourceName);
/**
* Return true if the resource is included by the filter.
* @param resourceName the resource name
* @return true if the resource is excluded
*/
boolean isResourceExcluded(String resourceName);
}

@ -1,39 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.io.Serializable;
import org.springframework.boot.loader.tools.Layer;
/**
* A strategy used to match a resource to a layer.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public interface ResourceStrategy extends Serializable {
/**
* Return a {@link Layer} for the given resource. If no matching layer is found,
* {@code null} is returned.
* @param resourceName the name of the resource
* @return the matching layer or {@code null}
*/
Layer getMatchingLayer(String resourceName);
}

@ -1,84 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
/**
* An implementation of {@link LibraryFilter} based on the library's coordinates.
*
* @author Madhura Bhave
* @author Scott Frederick
* @since 2.3.0
*/
public class CoordinateFilter implements LibraryFilter {
private static final String EMPTY_COORDINATES = "::";
private final List<Pattern> includes;
private final List<Pattern> excludes;
public CoordinateFilter(List<String> includes, List<String> excludes) {
this.includes = includes.stream().map(this::asPattern).collect(Collectors.toList());
this.excludes = excludes.stream().map(this::asPattern).collect(Collectors.toList());
}
private Pattern asPattern(String string) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
if (c == '.') {
builder.append("\\.");
}
else if (c == '*') {
builder.append(".*");
}
else {
builder.append(c);
}
}
return Pattern.compile(builder.toString());
}
@Override
public boolean isLibraryIncluded(Library library) {
return isMatch(library, this.includes);
}
@Override
public boolean isLibraryExcluded(Library library) {
return isMatch(library, this.excludes);
}
private boolean isMatch(Library library, List<Pattern> patterns) {
LibraryCoordinates coordinates = library.getCoordinates();
String input = (coordinates != null) ? coordinates.toString() : EMPTY_COORDINATES;
for (Pattern pattern : patterns) {
if (pattern.matcher(input).matches()) {
return true;
}
}
return false;
}
}

@ -1,62 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Library;
import org.springframework.util.Assert;
/**
* A {@link LibraryStrategy} with custom filters.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class FilteredLibraryStrategy implements LibraryStrategy {
private final Layer layer;
private final List<LibraryFilter> filters = new ArrayList<>();
public FilteredLibraryStrategy(String layer, List<LibraryFilter> filters) {
Assert.notEmpty(filters, "Filters should not be empty for custom strategy.");
this.layer = new Layer(layer);
this.filters.addAll(filters);
}
public Layer getLayer() {
return this.layer;
}
@Override
public Layer getMatchingLayer(Library library) {
boolean isIncluded = false;
for (LibraryFilter filter : this.filters) {
if (filter.isLibraryExcluded(library)) {
return null;
}
if (!isIncluded && filter.isLibraryIncluded(library)) {
isIncluded = true;
}
}
return (isIncluded) ? this.layer : null;
}
}

@ -1,45 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.io.Serializable;
import org.springframework.boot.loader.tools.Library;
/**
* A filter that can tell if a {@link Library} has been included or excluded.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public interface LibraryFilter extends Serializable {
/**
* Return true if the {@link Library} is included by the filter.
* @param library the library
* @return true if the library is included
*/
boolean isLibraryIncluded(Library library);
/**
* Return true if the {@link Library} is excluded by the filter.
* @param library the library
* @return true if the library is excluded
*/
boolean isLibraryExcluded(Library library);
}

@ -1,21 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for custom layers for everything in BOOT-INF/lib.
*
*/
package org.springframework.boot.loader.tools.layer.library;

@ -34,6 +34,7 @@ import java.util.Set;
import java.util.jar.Attributes; import java.util.jar.Attributes;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -650,6 +651,11 @@ abstract class AbstractPackagerTests<P extends Packager> {
return this.layers.iterator(); return this.layers.iterator();
} }
@Override
public Stream<Layer> stream() {
return this.layers.stream();
}
@Override @Override
public Layer getLayer(String name) { public Layer getLayer(String name) {
return DEFAULT_LAYER; return DEFAULT_LAYER;

@ -19,56 +19,42 @@ package org.springframework.boot.loader.tools;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link LibraryCoordinates}. * Tests for {@link LibraryCoordinates}.
* *
* @author Scott Frederick * @author Phillip Webb
*/ */
class LibraryCoordinatesTests { class LibraryCoordinatesTests {
@Test @Test
void parseCoordinatesWithAllElements() { void ofCreateLibraryCoordinates() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0"); LibraryCoordinates coordinates = LibraryCoordinates.of("g", "a", "v");
assertThat(coordinates.getGroupId()).isEqualTo("com.acme"); assertThat(coordinates.getGroupId()).isEqualTo("g");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); assertThat(coordinates.getArtifactId()).isEqualTo("a");
assertThat(coordinates.getVersion()).isEqualTo("1.0.0"); assertThat(coordinates.getVersion()).isEqualTo("v");
assertThat(coordinates.toString()).isEqualTo("g:a:v");
} }
@Test @Test
void parseCoordinatesWithoutVersion() { void toStandardNotationStringWhenCoordinatesAreNull() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library"); assertThat(LibraryCoordinates.toStandardNotationString(null)).isEqualTo("::");
assertThat(coordinates.getGroupId()).isEqualTo("com.acme");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isNull();
} }
@Test @Test
void parseCoordinatesWithEmptyElements() { void toStandardNotationStringWhenCoordinatesElementsNull() {
LibraryCoordinates coordinates = new LibraryCoordinates(":my-library:"); assertThat(LibraryCoordinates.toStandardNotationString(mock(LibraryCoordinates.class))).isEqualTo("::");
assertThat(coordinates.getGroupId()).isEqualTo("");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isNull();
} }
@Test @Test
void parseCoordinatesWithExtraElements() { void toStandardNotationString() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0.BUILD-SNAPSHOT:11111"); LibraryCoordinates coordinates = mock(LibraryCoordinates.class);
assertThat(coordinates.getGroupId()).isEqualTo("com.acme"); given(coordinates.getGroupId()).willReturn("a");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); given(coordinates.getArtifactId()).willReturn("b");
assertThat(coordinates.getVersion()).isEqualTo("1.0.0.BUILD-SNAPSHOT"); given(coordinates.getVersion()).willReturn("c");
} assertThat(LibraryCoordinates.toStandardNotationString(coordinates)).isEqualTo("a:b:c");
@Test
void parseCoordinatesWithoutMinimumElements() {
assertThatIllegalArgumentException().isThrownBy(() -> new LibraryCoordinates("com.acme"));
}
@Test
void toStringReturnsString() {
assertThat(new LibraryCoordinates("com.acme:my-library:1.0.0")).hasToString("com.acme:my-library:1.0.0");
assertThat(new LibraryCoordinates("com.acme:my-library")).hasToString("com.acme:my-library:");
} }
} }

@ -0,0 +1,57 @@
/*
* 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.loader.tools.layer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ApplicationContentFilter}.
*
* @author Madhura Bhave
* @author Stephane Nicoll
* @author Phillip Webb
*/
class ApplicationContentFilterTests {
@Test
void createWhenPatternIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ApplicationContentFilter(null))
.withMessage("Pattern must not be empty");
}
@Test
void createWhenPatternIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ApplicationContentFilter(""))
.withMessage("Pattern must not be empty");
}
@Test
void matchesWhenWildcardPatternMatchesReturnsTrue() {
ApplicationContentFilter filter = new ApplicationContentFilter("META-INF/**");
assertThat(filter.matches("META-INF/resources/application.yml")).isTrue();
}
@Test
void matchesWhenWildcardPatternDoesNotMatchReturnsFalse() {
ApplicationContentFilter filter = new ApplicationContentFilter("META-INF/**");
assertThat(filter.matches("src/main/resources/application.yml")).isFalse();
}
}

@ -1,126 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.boot.loader.tools.layer.application.FilteredResourceStrategy;
import org.springframework.boot.loader.tools.layer.application.LocationFilter;
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
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 CustomLayers}.
*
* @author Stephane Nicoll
*/
class CustomLayersTests {
@Test
void customLayersAreAvailable() {
Layer first = new Layer("first");
Layer second = new Layer("second");
CustomLayers customLayers = new CustomLayers(Arrays.asList(first, second), Collections.emptyList(),
Collections.emptyList());
List<Layer> actualLayers = new ArrayList<>();
customLayers.iterator().forEachRemaining(actualLayers::add);
assertThat(actualLayers).containsExactly(first, second);
}
@Test
void layerForResourceIsFound() {
FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test", Collections
.singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList())));
Layer targetLayer = new Layer("test");
CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer),
Collections.singletonList(resourceStrategy), Collections.emptyList());
assertThat(customLayers.getLayer("META-INF/manifest.mf")).isNotNull().isEqualTo(targetLayer);
}
@Test
void layerForResourceIsNotFound() {
FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test", Collections
.singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList())));
CustomLayers customLayers = new CustomLayers(Collections.singletonList(new Layer("test")),
Collections.singletonList(resourceStrategy), Collections.emptyList());
assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer("com/acme"));
}
@Test
void layerForResourceIsNotInListedLayers() {
FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test-not-listed", Collections
.singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList())));
Layer targetLayer = new Layer("test");
CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer),
Collections.singletonList(resourceStrategy), Collections.emptyList());
assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer("META-INF/manifest.mf"))
.withMessageContaining("META-INF/manifest.mf").withMessageContaining("test-not-listed")
.withMessageContaining("[test]");
}
@Test
void layerForLibraryIsFound() {
FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test", Collections
.singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList())));
Layer targetLayer = new Layer("test");
CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), Collections.emptyList(),
Collections.singletonList(libraryStrategy));
assertThat(customLayers.getLayer(mockLibrary("com.acme:test"))).isNotNull().isEqualTo(targetLayer);
}
@Test
void layerForLibraryIsNotFound() {
FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test", Collections
.singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList())));
CustomLayers customLayers = new CustomLayers(Collections.singletonList(new Layer("test")),
Collections.emptyList(), Collections.singletonList(libraryStrategy));
assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer(mockLibrary("org.another:test")));
}
@Test
void layerForLibraryIsNotInListedLayers() {
FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test-not-listed", Collections
.singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList())));
Layer targetLayer = new Layer("test");
CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), Collections.emptyList(),
Collections.singletonList(libraryStrategy));
assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer(mockLibrary("com.acme:test")))
.withMessageContaining("com.acme:test").withMessageContaining("test-not-listed")
.withMessageContaining("[test]");
}
private Library mockLibrary(String coordinates) {
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates(coordinates));
given(library.getName()).willReturn(coordinates);
return library;
}
}

@ -0,0 +1,141 @@
/*
* 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.loader.tools.layer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.Layer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link IncludeExcludeContentSelector}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class IncludeExcludeContentSelectorTests {
private static final Layer LAYER = new Layer("test");
@Test
void createWhenLayerIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(
() -> new IncludeExcludeContentSelector<>(null, Collections.emptyList(), Collections.emptyList()))
.withMessage("Layer must not be null");
}
@Test
void createWhenFactoryIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new IncludeExcludeContentSelector<>(LAYER, null, null, null))
.withMessage("FilterFactory must not be null");
}
@Test
void getLayerReturnsLayer() {
IncludeExcludeContentSelector<?> selector = new IncludeExcludeContentSelector<>(LAYER, null, null);
assertThat(selector.getLayer()).isEqualTo(LAYER);
}
@Test
void containsWhenEmptyIncludesAndEmptyExcludesReturnsTrue() {
List<String> includes = Arrays.asList();
List<String> excludes = Arrays.asList();
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("A")).isTrue();
}
@Test
void containsWhenNullIncludesAndEmptyExcludesReturnsTrue() {
List<String> includes = null;
List<String> excludes = null;
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("A")).isTrue();
}
@Test
void containsWhenEmptyIncludesAndNotExcludedReturnsTrue() {
List<String> includes = Arrays.asList();
List<String> excludes = Arrays.asList("B");
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("A")).isTrue();
}
@Test
void containsWhenEmptyIncludesAndExcludedReturnsFalse() {
List<String> includes = Arrays.asList();
List<String> excludes = Arrays.asList("A");
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("A")).isFalse();
}
@Test
void containsWhenIncludedAndEmptyExcludesReturnsTrue() {
List<String> includes = Arrays.asList("A", "B");
List<String> excludes = Arrays.asList();
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("B")).isTrue();
}
@Test
void containsWhenIncludedAndNotExcludedReturnsTrue() {
List<String> includes = Arrays.asList("A", "B");
List<String> excludes = Arrays.asList("C", "D");
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("B")).isTrue();
}
@Test
void containsWhenIncludedAndExcludedReturnsFalse() {
List<String> includes = Arrays.asList("A", "B");
List<String> excludes = Arrays.asList("C", "D");
IncludeExcludeContentSelector<String> selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes,
TestContentsFilter::new);
assertThat(selector.contains("C")).isFalse();
}
/**
* {@link ContentFilter} used for testing.
*/
static class TestContentsFilter implements ContentFilter<String> {
private final String match;
TestContentsFilter(String match) {
this.match = match;
}
@Override
public boolean matches(String item) {
return this.match.equals(item);
}
}
}

@ -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.loader.tools.layer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LibraryContentFilter}.
*
* @author Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
*/
class LibraryContentFilterTests {
@Test
void createWhenCoordinatesPatternIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new LibraryContentFilter(null))
.withMessage("CoordinatesPattern must not be empty");
}
@Test
void createWhenCoordinatesPatternIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new LibraryContentFilter(""))
.withMessage("CoordinatesPattern must not be empty");
}
@Test
void matchesWhenGroupIdIsNullAndToMatchHasWildcardReturnsTrue() {
LibraryContentFilter filter = new LibraryContentFilter("*:*");
assertThat(filter.matches(mockLibrary(null, null, null))).isTrue();
}
@Test
void matchesWhenArtifactIdIsNullAndToMatchHasWildcardReturnsTrue() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:*");
assertThat(filter.matches(mockLibrary("org.acme", null, null))).isTrue();
}
@Test
void matchesWhenVersionIsNullAndToMatchHasWildcardReturnsTrue() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:something:*");
assertThat(filter.matches(mockLibrary("org.acme", "something", null))).isTrue();
}
@Test
void matchesWhenGroupIdDoesNotMatchReturnsFalse() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:*");
assertThat(filter.matches(mockLibrary("other.foo", null, null))).isFalse();
}
@Test
void matchesWhenWhenArtifactIdDoesNotMatchReturnsFalse() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*");
assertThat(filter.matches(mockLibrary("org.acme", "other", null))).isFalse();
}
@Test
void matchesWhenArtifactIdMatchesReturnsTrue() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*");
assertThat(filter.matches(mockLibrary("org.acme", "test", null))).isTrue();
}
@Test
void matchesWhenVersionDoesNotMatchReturnsFalse() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*SNAPSHOT");
assertThat(filter.matches(mockLibrary("org.acme", "test", "1.0.0"))).isFalse();
}
@Test
void matchesWhenVersionMatchesReturnsTrue() {
LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*SNAPSHOT");
assertThat(filter.matches(mockLibrary("org.acme", "test", "1.0.0-SNAPSHOT"))).isTrue();
}
private Library mockLibrary(String groupId, String artifactId, String version) {
return mockLibrary(LibraryCoordinates.of(groupId, artifactId, version));
}
private Library mockLibrary(LibraryCoordinates coordinates) {
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(coordinates);
return library;
}
}

@ -1,104 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link FilteredResourceStrategy}.
*
* @author Madhura Bhave
*/
class FilteredResourceStrategyTests {
@Test
void createWhenFiltersNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new FilteredResourceStrategy("custom", null));
}
@Test
void createWhenFiltersEmptyShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new FilteredResourceStrategy("custom", Collections.emptyList()));
}
@Test
void getLayerShouldReturnLayerName() {
FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom",
Collections.singletonList(new TestFilter1()));
assertThat(strategy.getLayer().toString()).isEqualTo("custom");
}
@Test
void getMatchingLayerWhenFilterMatchesIncludes() {
FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom",
Collections.singletonList(new TestFilter1()));
assertThat(strategy.getMatchingLayer("ABCD").toString()).isEqualTo("custom");
}
@Test
void matchesWhenFilterMatchesIncludesAndExcludesFromSameFilter() {
FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom",
Collections.singletonList(new TestFilter1()));
assertThat(strategy.getMatchingLayer("AZ")).isNull();
}
@Test
void matchesWhenFilterMatchesIncludesAndExcludesFromAnotherFilter() {
List<ResourceFilter> filters = new ArrayList<>();
filters.add(new TestFilter1());
filters.add(new TestFilter2());
FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom", filters);
assertThat(strategy.getMatchingLayer("AY")).isNull();
}
private static class TestFilter1 implements ResourceFilter {
@Override
public boolean isResourceIncluded(String resourceName) {
return resourceName.startsWith("A");
}
@Override
public boolean isResourceExcluded(String resourceName) {
return resourceName.endsWith("Z");
}
}
private static class TestFilter2 implements ResourceFilter {
@Override
public boolean isResourceIncluded(String resourceName) {
return resourceName.startsWith("B");
}
@Override
public boolean isResourceExcluded(String resourceName) {
return resourceName.endsWith("Y");
}
}
}

@ -1,57 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LocationFilter}.
*
* @author Madhura Bhave
* @author Stephane Nicoll
*/
class LocationFilterTests {
@Test
void isResourceIncludedWhenPatternMatchesWithWildcard() {
LocationFilter filter = new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList());
assertThat(filter.isResourceIncluded("META-INF/resources/application.yml")).isTrue();
}
@Test
void isResourceIncludedWhenPatternDoesNotMatch() {
LocationFilter filter = new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList());
assertThat(filter.isResourceIncluded("src/main/resources/application.yml")).isFalse();
}
@Test
void isResourceExcludedWhenPatternMatchesWithWildcard() {
LocationFilter filter = new LocationFilter(Collections.emptyList(), Collections.singletonList("META-INF/**"));
assertThat(filter.isResourceExcluded("META-INF/resources/application.yml")).isTrue();
}
@Test
void isResourceExcludedWhenPatternDoesNotMatch() {
LocationFilter filter = new LocationFilter(Collections.emptyList(), Collections.singletonList("META-INF/**"));
assertThat(filter.isResourceExcluded("src/main/resources/application.yml")).isFalse();
}
}

@ -1,111 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CoordinateFilter}.
*
* @author Madhura Bhave
* @author Scott Frederick
*/
class CoordinateFilterTests {
@Test
void isLibraryIncludedWhenGroupIdIsNullAndToMatchHasWildcard() {
List<String> includes = Collections.singletonList("*:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates(null, null, null));
assertThat(filter.isLibraryIncluded(library)).isTrue();
}
@Test
void isLibraryIncludedWhenArtifactIdIsNullAndToMatchHasWildcard() {
List<String> includes = Collections.singletonList("org.acme:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", null, null));
assertThat(filter.isLibraryIncluded(library)).isTrue();
}
@Test
void isLibraryIncludedWhenVersionIsNullAndToMatchHasWildcard() {
List<String> includes = Collections.singletonList("org.acme:something:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "something", null));
assertThat(filter.isLibraryIncluded(library)).isTrue();
}
@Test
void isLibraryIncludedWhenGroupIdDoesNotMatch() {
List<String> includes = Collections.singletonList("org.acme:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("other.foo", null, null));
assertThat(filter.isLibraryIncluded(library)).isFalse();
}
@Test
void isLibraryIncludedWhenArtifactIdDoesNotMatch() {
List<String> includes = Collections.singletonList("org.acme:test:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "other", null));
assertThat(filter.isLibraryIncluded(library)).isFalse();
}
@Test
void isLibraryIncludedWhenArtifactIdMatches() {
List<String> includes = Collections.singletonList("org.acme:test:*");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", null));
assertThat(filter.isLibraryIncluded(library)).isTrue();
}
@Test
void isLibraryIncludedWhenVersionDoesNotMatch() {
List<String> includes = Collections.singletonList("org.acme:test:*SNAPSHOT");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", "1.0.0"));
assertThat(filter.isLibraryIncluded(library)).isFalse();
}
@Test
void isLibraryIncludedWhenVersionMatches() {
List<String> includes = Collections.singletonList("org.acme:test:*SNAPSHOT");
CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList());
Library library = mock(Library.class);
given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", "1.0.0-SNAPSHOT"));
assertThat(filter.isLibraryIncluded(library)).isTrue();
}
}

@ -1,119 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryScope;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link FilteredLibraryStrategy}.
*
* @author Madhura Bhave
*/
class FilteredLibraryStrategyTests {
@Test
void createWhenFiltersNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new FilteredLibraryStrategy("custom", null));
}
@Test
void createWhenFiltersEmptyShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new FilteredLibraryStrategy("custom", Collections.emptyList()));
}
@Test
void getLayerShouldReturnLayerName() {
FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom",
Collections.singletonList(new TestFilter1Library()));
assertThat(strategy.getLayer().toString()).isEqualTo("custom");
}
@Test
void getMatchingLayerWhenFilterMatchesIncludes() {
FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom",
Collections.singletonList(new TestFilter1Library()));
Library library = mockLibrary("A-Compile", LibraryScope.COMPILE);
assertThat(strategy.getMatchingLayer(library).toString()).isEqualTo("custom");
}
@Test
void matchesWhenFilterMatchesIncludesAndExcludesFromSameFilter() {
FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom",
Collections.singletonList(new TestFilter1Library()));
Library library = mockLibrary("A-Runtime", LibraryScope.RUNTIME);
assertThat(strategy.getMatchingLayer(library)).isNull();
}
@Test
void matchesWhenFilterMatchesIncludesAndExcludesFromAnotherFilter() {
List<LibraryFilter> filters = new ArrayList<>();
filters.add(new TestFilter1Library());
filters.add(new TestFilter2Library());
FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom", filters);
Library library = mockLibrary("A-Provided", LibraryScope.PROVIDED);
assertThat(strategy.getMatchingLayer(library)).isNull();
}
private Library mockLibrary(String name, LibraryScope runtime) {
Library library = mock(Library.class);
given(library.getName()).willReturn(name);
given(library.getScope()).willReturn(runtime);
return library;
}
private static class TestFilter1Library implements LibraryFilter {
@Override
public boolean isLibraryIncluded(Library library) {
return library.getName().contains("A");
}
@Override
public boolean isLibraryExcluded(Library library) {
return library.getScope().equals(LibraryScope.RUNTIME);
}
}
private static class TestFilter2Library implements LibraryFilter {
@Override
public boolean isLibraryIncluded(Library library) {
return library.getName().contains("B");
}
@Override
public boolean isLibraryExcluded(Library library) {
return library.getScope().equals(LibraryScope.PROVIDED);
}
}
}

@ -26,6 +26,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -296,8 +297,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/") assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot") .hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith( .hasEntryWithNameStartingWith(jarModeLayerTools());
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}); });
} }
@ -308,8 +308,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/") assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot") .hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.doesNotHaveEntryWithNameStartingWith( .doesNotHaveEntryWithNameStartingWith(jarModeLayerTools());
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
}); });
} }
@ -333,6 +332,13 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(firstHash).isEqualTo(secondHash); assertThat(firstHash).isEqualTo(secondHash);
} }
private String jarModeLayerTools() {
JarModeLibrary library = JarModeLibrary.LAYER_TOOLS;
String version = library.getCoordinates().getVersion();
String layer = (version == null || !version.contains("SNAPSHOT")) ? "dependencies" : "snapshot-dependencies";
return "BOOT-INF/layers/" + layer + "/lib/" + library.getName();
}
private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) { private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) {
AtomicReference<String> jarHash = new AtomicReference<>(); AtomicReference<String> jarHash = new AtomicReference<>();
mavenBuild.project("jar-output-timestamp").execute((project) -> { mavenBuild.project("jar-output-timestamp").execute((project) -> {

@ -1,35 +1,23 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers" <layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/layers/layers-configuration-2.3.xsd"> https://www.springframework.org/schema/layers/layers-2.3.xsd">
<layers> <application>
<into layer="configuration">
<include>**/application*.*</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="snapshot-dependencies">
<include>*:*:*-SNAPSHOT</include>
</into>
<into layer="my-dependencies-name" />
</dependencies>
<layerOrder>
<layer>configuration</layer> <layer>configuration</layer>
<layer>application</layer> <layer>application</layer>
<layer>snapshot-dependencies</layer> <layer>snapshot-dependencies</layer>
<layer>my-dependencies-name</layer> <layer>my-dependencies-name</layer>
</layers> </layerOrder>
<libraries> </layers>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<application>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</application>
</layers-configuration>

@ -48,6 +48,7 @@ import org.springframework.boot.loader.tools.Layouts.None;
import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Layouts.War;
import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Packager; import org.springframework.boot.loader.tools.Packager;
import org.springframework.boot.loader.tools.layer.CustomLayers;
/** /**
* Abstract base class for classes that work with an {@link Packager}. * Abstract base class for classes that work with an {@link Packager}.
@ -135,15 +136,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
} }
if (this.layers != null && this.layers.isEnabled()) { if (this.layers != null && this.layers.isEnabled()) {
if (this.layers.getConfiguration() != null) { if (this.layers.getConfiguration() != null) {
try { packager.setLayers(getCustomLayers(this.layers.getConfiguration()));
Document document = getDocumentIfAvailable(this.layers.getConfiguration());
CustomLayersProvider customLayersProvider = new CustomLayersProvider();
packager.setLayers(customLayersProvider.getLayers(document));
}
catch (Exception ex) {
throw new IllegalStateException("Failed to process custom layers configuration "
+ this.layers.getConfiguration().getAbsolutePath(), ex);
}
} }
packager.setLayout(new LayeredJar()); packager.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools()); packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools());
@ -151,8 +144,19 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
return packager; return packager;
} }
private Document getDocumentIfAvailable(File configurationFile) throws Exception { private CustomLayers getCustomLayers(File configuration) {
InputSource inputSource = new InputSource(new FileInputStream(configurationFile)); try {
Document document = getDocumentIfAvailable(configuration);
return new CustomLayersProvider().getLayers(document);
}
catch (Exception ex) {
throw new IllegalStateException(
"Failed to process custom layers configuration " + configuration.getAbsolutePath(), ex);
}
}
private Document getDocumentIfAvailable(File xmlFile) throws Exception {
InputSource inputSource = new InputSource(new FileInputStream(xmlFile));
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder(); DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(inputSource); return builder.parse(inputSource);

@ -80,8 +80,7 @@ public class ArtifactsLibraries implements Libraries {
name = artifact.getGroupId() + "-" + name; name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name); this.log.debug("Renamed to: " + name);
} }
LibraryCoordinates coordinates = new LibraryCoordinates(artifact.getGroupId(), artifact.getArtifactId(), LibraryCoordinates coordinates = new ArtifactLibraryCoordinates(artifact);
artifact.getVersion());
callback.library(new Library(name, artifact.getFile(), scope, coordinates, isUnpackRequired(artifact))); callback.library(new Library(name, artifact.getFile(), scope, coordinates, isUnpackRequired(artifact)));
} }
} }
@ -122,4 +121,37 @@ public class ArtifactsLibraries implements Libraries {
return sb.toString(); return sb.toString();
} }
/**
* {@link LibraryCoordinates} backed by a Maven {@link Artifact}.
*/
private static class ArtifactLibraryCoordinates implements LibraryCoordinates {
private final Artifact artifact;
ArtifactLibraryCoordinates(Artifact artifact) {
this.artifact = artifact;
}
@Override
public String getGroupId() {
return this.artifact.getGroupId();
}
@Override
public String getArtifactId() {
return this.artifact.getArtifactId();
}
@Override
public String getVersion() {
return this.artifact.getVersion();
}
@Override
public String toString() {
return this.artifact.toString();
}
}
} }

@ -17,8 +17,10 @@
package org.springframework.boot.maven; package org.springframework.boot.maven;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Function;
import java.util.stream.Collectors;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@ -26,113 +28,76 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.Layer;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.layer.ApplicationContentFilter;
import org.springframework.boot.loader.tools.layer.ContentFilter;
import org.springframework.boot.loader.tools.layer.ContentSelector;
import org.springframework.boot.loader.tools.layer.CustomLayers; import org.springframework.boot.loader.tools.layer.CustomLayers;
import org.springframework.boot.loader.tools.layer.application.FilteredResourceStrategy; import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector;
import org.springframework.boot.loader.tools.layer.application.LocationFilter; import org.springframework.boot.loader.tools.layer.LibraryContentFilter;
import org.springframework.boot.loader.tools.layer.application.ResourceFilter;
import org.springframework.boot.loader.tools.layer.application.ResourceStrategy;
import org.springframework.boot.loader.tools.layer.library.CoordinateFilter;
import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy;
import org.springframework.boot.loader.tools.layer.library.LibraryFilter;
import org.springframework.boot.loader.tools.layer.library.LibraryStrategy;
import org.springframework.util.Assert;
/** /**
* Produces a {@link CustomLayers} based on the given {@link Document}. * Produces a {@link CustomLayers} based on the given {@link Document}.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.3.0 * @author Phillip Webb
*/ */
public class CustomLayersProvider { class CustomLayersProvider {
public CustomLayers getLayers(Document document) { CustomLayers getLayers(Document document) {
Element root = document.getDocumentElement(); Element root = document.getDocumentElement();
NodeList nodes = root.getChildNodes(); List<ContentSelector<String>> applicationSelectors = getApplicationSelectors(root);
List<Layer> layers = new ArrayList<>(); List<ContentSelector<Library>> librarySelectors = getLibrarySelectors(root);
List<LibraryStrategy> libraryStrategies = new ArrayList<>(); List<Layer> layers = getLayers(root);
List<ResourceStrategy> resourceStrategies = new ArrayList<>(); return new CustomLayers(layers, applicationSelectors, librarySelectors);
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node instanceof Element) {
processNode(layers, libraryStrategies, resourceStrategies, (Element) node);
}
}
return new CustomLayers(layers, resourceStrategies, libraryStrategies);
} }
private void processNode(List<Layer> layers, List<LibraryStrategy> libraryStrategies, private List<ContentSelector<String>> getApplicationSelectors(Element root) {
List<ResourceStrategy> resourceStrategies, Element node) { return getSelectors(root, "application", ApplicationContentFilter::new);
String nodeName = node.getNodeName();
if ("layers".equals(nodeName)) {
layers.addAll(getLayers(node));
}
NodeList contents = node.getChildNodes();
if ("libraries".equals(nodeName)) {
libraryStrategies.addAll(getStrategies(contents,
(StrategyFactory<LibraryFilter, LibraryStrategy>) FilteredLibraryStrategy::new,
CoordinateFilter::new, "coordinates"::equals));
}
if ("application".equals(nodeName)) {
resourceStrategies.addAll(getStrategies(contents,
(StrategyFactory<ResourceFilter, ResourceStrategy>) FilteredResourceStrategy::new,
LocationFilter::new, "locations"::equals));
}
} }
private List<Layer> getLayers(Element element) { private List<ContentSelector<Library>> getLibrarySelectors(Element root) {
List<Layer> layers = new ArrayList<>(); return getSelectors(root, "dependencies", LibraryContentFilter::new);
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node childNode = childNodes.item(i);
if (childNode instanceof Element) {
Element childElement = (Element) childNode;
if ("layer".equals(childElement.getNodeName())) {
layers.add(new Layer(childElement.getTextContent()));
}
}
}
return layers;
} }
private <T, E> List<T> getStrategies(NodeList nodes, StrategyFactory<E, T> strategyFactory, private List<Layer> getLayers(Element root) {
FilterFactory<E> filterFactory, Predicate<String> filterPredicate) { Element layerOrder = getChildElement(root, "layerOrder");
List<T> contents = new ArrayList<>(); if (layerOrder == null) {
for (int i = 0; i < nodes.getLength(); i++) { return Collections.emptyList();
Node node = nodes.item(i);
if (node instanceof Element) {
Element element = (Element) node;
if ("layer-content".equals(element.getTagName())) {
List<E> filters = getFilters(node, filterFactory, filterPredicate);
String layer = element.getAttribute("layer");
contents.add(strategyFactory.getStrategy(layer, filters));
}
}
} }
return contents; return getChildNodeTextContent(layerOrder, "layer").stream().map(Layer::new).collect(Collectors.toList());
} }
private <E> List<E> getFilters(Node node, FilterFactory<E> factory, Predicate<String> predicate) { private <T> List<ContentSelector<T>> getSelectors(Element root, String elementName,
NodeList childNodes = node.getChildNodes(); Function<String, ContentFilter<T>> filterFactory) {
Assert.state(childNodes.getLength() > 0, "Filters for layer-content must not be empty."); Element element = getChildElement(root, elementName);
List<E> filters = new ArrayList<>(); if (element == null) {
for (int i = 0; i < childNodes.getLength(); i++) { return Collections.emptyList();
Node childNode = childNodes.item(i); }
if (childNode instanceof Element) { List<ContentSelector<T>> selectors = new ArrayList<>();
List<String> include = getPatterns((Element) childNode, "include"); NodeList children = element.getChildNodes();
List<String> exclude = getPatterns((Element) childNode, "exclude"); for (int i = 0; i < children.getLength(); i++) {
if (predicate.test(childNode.getNodeName())) { Node child = children.item(i);
filters.add(factory.getFilter(include, exclude)); if (child instanceof Element) {
} ContentSelector<T> selector = getSelector((Element) child, filterFactory);
selectors.add(selector);
} }
} }
return filters; return selectors;
}
private <T> ContentSelector<T> getSelector(Element element, Function<String, ContentFilter<T>> filterFactory) {
Layer layer = new Layer(element.getAttribute("layer"));
List<String> includes = getChildNodeTextContent(element, "include");
List<String> excludes = getChildNodeTextContent(element, "exclude");
return new IncludeExcludeContentSelector<>(layer, includes, excludes, filterFactory);
} }
private List<String> getPatterns(Element element, String key) { private List<String> getChildNodeTextContent(Element element, String tagName) {
List<String> patterns = new ArrayList<>(); List<String> patterns = new ArrayList<>();
NodeList nodes = element.getElementsByTagName(key); NodeList nodes = element.getElementsByTagName(tagName);
for (int j = 0; j < nodes.getLength(); j++) { for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(j); Node node = nodes.item(i);
if (node instanceof Element) { if (node instanceof Element) {
patterns.add(node.getTextContent()); patterns.add(node.getTextContent());
} }
@ -140,16 +105,15 @@ public class CustomLayersProvider {
return patterns; return patterns;
} }
interface StrategyFactory<E, T> { private Element getChildElement(Element element, String tagName) {
NodeList nodes = element.getElementsByTagName(tagName);
T getStrategy(String layer, List<E> filters); if (nodes.getLength() == 0) {
return null;
} }
if (nodes.getLength() > 1) {
interface FilterFactory<E> { throw new IllegalStateException("Multiple '" + tagName + "' nodes found");
}
E getFilter(List<String> includes, List<String> excludes); return (Element) nodes.item(0);
} }
} }

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema elementFormDefault="qualified"
xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/boot/layers">
<xsd:element name="layers" type="layersType" />
<xsd:complexType name="layersType">
<xsd:sequence>
<xsd:element name="application" type="applicationType" />
<xsd:element name="dependencies" type="dependenciesType" />
<xsd:element name="layerOrder" type="layerOrderType" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="applicationType">
<xsd:annotation>
<xsd:documentation><![CDATA[
The 'into layer' selections that should be applied to application classes and resources.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="into" type="intoType" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="dependenciesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
The 'into layer' selections that should be applied to dependencies.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="into" type="intoType" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="layerOrderType">
<xsd:annotation>
<xsd:documentation><![CDATA[
The order that layers should be added (starting with the least frequently changed layer).
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="layer" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The layer name.
]]></xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="intoType">
<xsd:choice maxOccurs="unbounded">
<xsd:element type="xsd:string" name="include"
minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to include.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element type="xsd:string" name="exclude"
minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to exclude.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
<xsd:attribute type="xsd:string" name="layer"
use="required" />
</xsd:complexType>
</xsd:schema>

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema elementFormDefault="qualified"
xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/boot/layers">
<xsd:element name="layers-configuration" type="layersConfigurationType"/>
<xsd:complexType name="layersConfigurationType">
<xsd:sequence>
<xsd:element type="layersType" name="layers"/>
<xsd:element type="librariesType" name="libraries" minOccurs="0"/>
<xsd:element type="applicationType" name="application" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="layersType">
<xsd:sequence>
<xsd:element type="xsd:string" name="layer" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of a layer. Each layer in the configuration must be referenced once and the
order matches how the content is likely to change. Put layers that are frequently
updated first, layers that are more stable (such as non-snapshot dependencies) last.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="librariesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to libraries. If no strategies are defined, two
layers are created out-of-the-box. A "snapshot-dependencies" layer with SNAPSHOT
libraries and a "dependencies" layer with all the other libraries.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="librariesLayerContentType" name="layer-content">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on libraries.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="librariesLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="coordinates" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="applicationType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to application classes and resources. If no strategies are defined, a single
"application" layer is created out-of-the-box.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="applicationLayerContentType" name="layer-content" maxOccurs="unbounded"
minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on application classes and resources.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="applicationLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="locations" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="filterType">
<xsd:sequence>
<xsd:element type="xsd:string" name="exclude" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to exclude. An exclude pattern takes precedence over an
include pattern and must be declared first.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element type="xsd:string" name="include" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to include.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

@ -52,8 +52,8 @@ public class CustomLayersProviderTests {
@Test @Test
void getLayerResolverWhenDocumentValid() throws Exception { void getLayerResolverWhenDocumentValid() throws Exception {
CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml")); CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml"));
assertThat(layers).extracting("name").containsExactly("configuration", "application", "my-resources", assertThat(layers).extracting("name").containsExactly("my-deps", "my-dependencies-name",
"snapshot-dependencies", "my-deps", "my-dependencies-name"); "snapshot-dependencies", "my-resources", "configuration", "application");
Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT"); Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT");
Library groupId = mockLibrary("my-library", "com.acme", null); Library groupId = mockLibrary("my-library", "com.acme", null);
Library otherDependency = mockLibrary("other-library", "org.foo", null); Library otherDependency = mockLibrary("other-library", "org.foo", null);
@ -68,22 +68,26 @@ public class CustomLayersProviderTests {
private Library mockLibrary(String name, String groupId, String version) { private Library mockLibrary(String name, String groupId, String version) {
Library library = mock(Library.class); Library library = mock(Library.class);
given(library.getName()).willReturn(name); given(library.getName()).willReturn(name);
given(library.getCoordinates()).willReturn(new LibraryCoordinates(groupId, null, version)); given(library.getCoordinates()).willReturn(LibraryCoordinates.of(groupId, null, version));
return library; return library;
} }
@Test @Test
void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() { void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() throws Exception {
assertThatIllegalStateException() CustomLayers layers = this.customLayersProvider.getLayers(getDocument("dependencies-layer-no-filter.xml"));
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("library-layer-no-filter.xml"))) Library library = mockLibrary("my-library", "com.acme", null);
.withMessage("Filters for layer-content must not be empty."); assertThat(layers.getLayer(library).toString()).isEqualTo("my-deps");
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer("application.yml"))
.withMessageContaining("match any layer");
} }
@Test @Test
void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() { void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() throws Exception {
assertThatIllegalStateException() CustomLayers layers = this.customLayersProvider.getLayers(getDocument("application-layer-no-filter.xml"));
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("resource-layer-no-filter.xml"))) Library library = mockLibrary("my-library", "com.acme", null);
.withMessage("Filters for layer-content must not be empty."); assertThat(layers.getLayer("application.yml").toString()).isEqualTo("my-layer");
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(library))
.withMessageContaining("match any layer");
} }
private Document getDocument(String resourceName) throws Exception { private Document getDocument(String resourceName) throws Exception {

@ -0,0 +1,11 @@
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers.xsd">
<application>
<into layer="my-layer" />
</application>
<layerOrder>
<layer>my-layer</layer>
</layerOrder>
</layers>

@ -0,0 +1,11 @@
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
<dependencies>
<into layer="my-deps" />
</dependencies>
<layerOrder>
<layer>my-deps</layer>
</layerOrder>
</layers>

@ -1,47 +1,32 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers" <layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration-2.3.xsd"> https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
<layers>
<layer>configuration</layer>
<layer>application</layer>
<layer>my-resources</layer>
<layer>snapshot-dependencies</layer>
<layer>my-deps</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-deps">
<coordinates>
<include>com.acme:*</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<application> <application>
<layer-content layer="my-resources"> <into layer="my-resources">
<locations> <include>META-INF/resources/**</include>
<include>META-INF/resources/**</include> <exclude>*.properties</exclude>
</locations> </into>
</layer-content> <into layer="configuration">
<layer-content layer="configuration"> <include>**/application*.*</include>
<locations> </into>
<include>**/application*.*</include> <into layer="application" />
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</application> </application>
</layers-configuration> <dependencies>
<into layer="snapshot-dependencies">
<include>*:*:*-SNAPSHOT</include>
</into>
<into layer="my-deps">
<include>com.acme:*</include>
</into>
<into layer="my-dependencies-name"/>
</dependencies>
<layerOrder>
<layer>my-deps</layer>
<layer>my-dependencies-name</layer>
<layer>snapshot-dependencies</layer>
<layer>my-resources</layer>
<layer>configuration</layer>
<layer>application</layer>
</layerOrder>
</layers>

@ -1,11 +0,0 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration-2.3.xsd">
<layers>
<layer>my-deps</layer>
</layers>
<libraries>
<layer-content layer="my-deps"/>
</libraries>
</layers-configuration>

@ -1,11 +1,11 @@
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers" <layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration-2.3.xsd"> https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
<layers>
<layer>my-layer</layer>
</layers>
<application> <application>
<layer-content layer="my-layer"/> <into layer="my-layer" />
</application> </application>
</layers-configuration> <layerOrder>
<layer>my-layer</layer>
</layerOrder>
</layers>
Loading…
Cancel
Save