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[]
bootJar {
layers {
layersOrder "dependencies", "snapshot-dependencies", "application"
libraries {
layerContent("snapshot-dependencies") {
coordinates {
include "*:*:*SNAPSHOT"
}
}
layerContent("dependencies") {
coordinates {
include "*:*"
}
}
}
layered {
application {
layerContent("application") {
locations {
include "**"
}
intoLayer("application")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
intoLayer("dependencies")
}
layerOrder "dependencies", "snapshot-dependencies", "application"
}
}
// end::layered[]

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

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

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

@ -27,7 +27,6 @@ import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact;
import org.gradle.api.plugins.ApplicationPlugin;
@ -94,9 +93,6 @@ final class JavaPluginAction implements PluginApplicationAction {
SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
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));
});
}

@ -27,6 +27,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
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.FileCopyDetailsInternal;
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.Specs;
import org.gradle.api.tasks.WorkResult;
@ -44,6 +46,9 @@ import org.gradle.api.tasks.util.PatternSet;
* Support class for implementations of {@link BootArchive}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @see BootJar
* @see BootWar
*/
class BootArchiveSupport {
@ -61,46 +66,71 @@ class BootArchiveSupport {
private final PatternSet requiresUnpack = new PatternSet();
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final PatternSet exclusions = new PatternSet();
private final String loaderMainClass;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private LaunchScriptConfiguration launchScript;
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.librarySpec = librarySpec;
this.compressionResolver = compressionResolver;
this.requiresUnpack.include(Specs.satisfyNone());
configureExclusions();
}
void configureManifest(Jar jar, String mainClassName, String springBootClasses, String springBootLib) {
Attributes attributes = jar.getManifest().getAttributes();
void configureManifest(Manifest manifest, String mainClass, String classes, String lib, String classPathIndex,
String layersIndex) {
Attributes attributes = manifest.getAttributes();
attributes.putIfAbsent("Main-Class", this.loaderMainClass);
attributes.putIfAbsent("Start-Class", mainClassName);
attributes.computeIfAbsent("Spring-Boot-Version", (key) -> determineSpringBootVersion());
attributes.putIfAbsent("Spring-Boot-Classes", springBootClasses);
attributes.putIfAbsent("Spring-Boot-Lib", springBootLib);
attributes.putIfAbsent("Start-Class", mainClass);
attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion());
if (classes != null) {
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() {
String implementationVersion = getClass().getPackage().getImplementationVersion();
return (implementationVersion != null) ? implementationVersion : "unknown";
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "unknown";
}
CopyAction createCopyAction(Jar jar) {
CopyAction copyAction = new BootZipCopyAction(jar.getArchiveFile().get().getAsFile(),
jar.isPreserveFileTimestamps(), isUsingDefaultLoader(jar), this.requiresUnpack.getAsSpec(),
this.exclusions.getAsExcludeSpec(), this.launchScript, this.compressionResolver,
jar.getMetadataCharset());
if (!jar.isReproducibleFileOrder()) {
return copyAction;
}
return new ReproducibleOrderingCopyAction(copyAction);
return createCopyAction(jar, null, false);
}
CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, boolean includeLayerTools) {
File output = jar.getArchiveFile().get().getAsFile();
Manifest manifest = jar.getManifest();
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
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) {
@ -132,7 +162,19 @@ class BootArchiveSupport {
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 (FileInputStream fileInputStream = new FileInputStream(file)) {
return isZip(fileInputStream);
@ -160,6 +202,17 @@ class BootArchiveSupport {
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 final CopyAction delegate;

@ -16,45 +16,24 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.BufferedWriter;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.ResolvableDependencies;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTreeElement;
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.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.bundling.Jar;
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;
import org.gradle.util.ConfigureUtil;
/**
* 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 Madhura Bhave
* @author Scott Frederick
* @author Phillip Webb
* @since 2.0.0
*/
public class BootJar extends Jar implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.JarLauncher",
this::resolveZipCompression);
private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher";
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.
*/
public BootJar() {
this.bootInf = getProject().copySpec().into("BOOT-INF");
getMainSpec().with(this.bootInf);
this.bootInf.into("classes", classpathFiles(File::isDirectory));
this.bootInf.into("lib", classpathFiles(File::isFile))
.eachFile((details) -> BootJar.this.dependencies.add(details.getPath()));
this.bootInf.into("",
(spec) -> spec.from((Callable<File>) () -> createClasspathIndex(BootJar.this.dependencies)));
this.bootInf.filesMatching("module-info.class",
this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
this.bootInfSpec = getProject().copySpec().into("BOOT-INF");
configureBootInfSpec(this.bootInfSpec);
getMainSpec().with(this.bootInfSpec);
}
private void configureBootInfSpec(CopySpec bootInfSpec) {
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()));
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) {
return (copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.classpath != null)
? this.classpath.filter(filter) : Collections.emptyList());
private Iterable<File> classpathDirectories() {
return classpathEntries(File::isDirectory);
}
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
public void copy() {
this.support.configureManifest(this, getMainClassName(), "BOOT-INF/classes/", "BOOT-INF/lib/");
Attributes attributes = this.getManifest().getAttributes();
if (this.layers != null) {
attributes.remove("Spring-Boot-Classes");
attributes.remove("Spring-Boot-Lib");
attributes.putIfAbsent("Spring-Boot-Layers-Index", "BOOT-INF/layers.idx");
if (this.layered != null) {
this.support.configureManifest(getManifest(), getMainClassName(), null, null, CLASSPATH_INDEX,
LAYERS_INDEX);
}
else {
this.support.configureManifest(getManifest(), getMainClassName(), CLASSES_FOLDER, LIB_FOLDER,
CLASSPATH_INDEX, null);
}
attributes.putIfAbsent("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx");
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
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);
}
@Internal
protected Iterable<Configuration> getConfigurations() {
return getProject().getConfigurations();
}
@Override
public String getMainClassName() {
if (this.mainClassName == null) {
@ -179,110 +164,28 @@ public class BootJar extends Jar implements BootArchive {
action.execute(enableLaunchScriptIfNecessary());
}
@Optional
@Nested
public LayerConfiguration getLayerConfiguration() {
return this.layerConfiguration;
}
/**
* 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()));
@Optional
public LayeredSpec getLayered() {
return this.layered;
}
private Layer layerForFileDetails(FileCopyDetails details) {
String path = details.getPath();
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;
public void layered() {
layered(true);
}
private File createLayersIndex() {
try {
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);
}
public void layered(boolean layered) {
this.layered = layered ? new LayeredSpec() : null;
}
private void resolveCoordinatesForFiles(ResolvableDependencies resolvableDependencies) {
ArtifactCollection resolvedArtifactResults = resolvableDependencies.getArtifacts();
Set<ResolvedArtifactResult> artifacts = resolvedArtifactResults.getArtifacts();
artifacts.forEach((artifact) -> this.coordinatesByFileName.put(artifact.getFile().getName(),
artifact.getId().getComponentIdentifier().getDisplayName()));
public void layered(Closure<?> closure) {
layered(ConfigureUtil.configureUsing(closure));
}
@Input
boolean isLayered() {
return this.layerConfiguration != null;
public void layered(Action<LayeredSpec> action) {
LayeredSpec layered = new LayeredSpec();
action.execute(layered);
this.layered = layered;
}
@Override
@ -317,13 +220,6 @@ public class BootJar extends Jar implements BootArchive {
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}
* directory of the jar.
@ -333,7 +229,7 @@ public class BootJar extends Jar implements BootArchive {
@Internal
public CopySpec getBootInf() {
CopySpec child = getProject().copySpec();
this.bootInf.with(child);
this.bootInfSpec.with(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
* represented by the given {@code details} to the jar.
* <p>
* By default, any file in {@code BOOT-INF/lib/} or
* {@code BOOT-INF/layers/<layer>/lib} is stored and all other files are deflated.
* @param details the details
* Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. By default, any
* {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* and all other files are {@link ZipCompression#DEFLATED deflated}.
* @param details the file copy details
* @return the compression to use
*/
protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String path = details.getRelativePath().getPathString();
for (String prefix : getLibPathPrefixes()) {
if (path.startsWith(prefix)) {
return ZipCompression.STORED;
}
}
return ZipCompression.DEFLATED;
return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
private Set<String> getLibPathPrefixes() {
if (this.layers == null) {
return Collections.singleton("BOOT-INF/lib/");
}
return StreamSupport.stream(this.layers.spliterator(), false)
.map((layer) -> "BOOT-INF/layers/" + layer + "/lib/").collect(Collectors.toSet());
/**
* Return if the {@link FileCopyDetails} are for a library. By default any file in
* {@code BOOT-INF/lib} 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);
}
private LaunchScriptConfiguration enableLaunchScriptIfNecessary() {
@ -387,4 +279,24 @@ public class BootJar extends Jar implements BootArchive {
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");
* you may not use this file except in compliance with the License.
@ -16,12 +16,12 @@
package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.util.Collections;
import java.util.concurrent.Callable;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileCopyDetails;
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.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class BootWar extends War implements BootArchive {
private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.WarLauncher",
this::resolveZipCompression);
private static final String LAUNCHER = "org.springframework.boot.loader.WarLauncher";
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;
@ -50,23 +58,19 @@ public class BootWar extends War implements BootArchive {
* Creates a new {@code BootWar} task.
*/
public BootWar() {
getWebInf().into("lib-provided",
(copySpec) -> copySpec.from((Callable<Iterable<File>>) () -> (this.providedClasspath != null)
? this.providedClasspath : Collections.emptyList()));
getRootSpec().filesMatching("module-info.class",
(details) -> details.setRelativePath(details.getRelativeSourcePath()));
getRootSpec().eachFile((details) -> {
String pathString = details.getRelativePath().getPathString();
if ((pathString.startsWith("WEB-INF/lib/") || pathString.startsWith("WEB-INF/lib-provided/"))
&& !this.support.isZip(details.getFile())) {
details.exclude();
}
});
this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression);
getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles));
this.support.moveModuleInfoToRoot(getRootSpec());
getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles);
}
private Object getProvidedLibFiles() {
return (this.providedClasspath != null) ? this.providedClasspath : Collections.emptyList();
}
@Override
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();
}
@ -171,20 +175,26 @@ public class BootWar extends War implements BootArchive {
}
/**
* Returns the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar.
* <p>
* By default, any file in {@code WEB-INF/lib/} or {@code WEB-INF/lib-provided/} is
* stored and all other files are deflated.
* @param details the details
* Return the {@link ZipCompression} that should be used when adding the file
* represented by the given {@code details} to the jar. By default, any
* {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored}
* and all other files are {@link ZipCompression#DEFLATED deflated}.
* @param details the file copy details
* @return the compression to use
*/
protected ZipCompression resolveZipCompression(FileCopyDetails details) {
String relativePath = details.getRelativePath().getPathString();
if (relativePath.startsWith("WEB-INF/lib/") || relativePath.startsWith("WEB-INF/lib-provided/")) {
return ZipCompression.STORED;
}
return ZipCompression.DEFLATED;
return isLibrary(details) ? ZipCompression.STORED : 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() {
@ -196,4 +206,24 @@ public class BootWar extends War implements BootArchive {
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");
* 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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.io.OutputStreamWriter;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
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.internal.file.copy.CopyAction;
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.tasks.WorkResult;
import org.gradle.api.tasks.WorkResults;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
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).
@ -49,69 +59,84 @@ import org.springframework.boot.loader.tools.FileUtils;
*/
class BootZipCopyAction implements CopyAction {
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0)
.getTimeInMillis();
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC)
.toInstant().toEpochMilli();
private final File output;
private final Manifest manifest;
private final boolean preserveFileTimestamps;
private final boolean includeDefaultLoader;
private final boolean includeLayerTools;
private final Spec<FileTreeElement> requiresUnpack;
private final Spec<FileTreeElement> exclusions;
private final LaunchScriptConfiguration launchScript;
private final Spec<FileCopyDetails> librarySpec;
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final String encoding;
BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
LaunchScriptConfiguration launchScript, Function<FileCopyDetails, ZipCompression> compressionResolver,
String encoding) {
private final LayerResolver layerResolver;
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader,
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.manifest = manifest;
this.preserveFileTimestamps = preserveFileTimestamps;
this.includeDefaultLoader = includeDefaultLoader;
this.includeLayerTools = includeLayerTools;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.launchScript = launchScript;
this.librarySpec = librarySpec;
this.compressionResolver = compressionResolver;
this.encoding = encoding;
this.layerResolver = layerResolver;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
public WorkResult execute(CopyActionProcessingStream copyActions) {
try {
writeArchive(stream);
return () -> true;
writeArchive(copyActions);
return WorkResults.didWork(true);
}
catch (IOException ex) {
throw new GradleException("Failed to create " + this.output, ex);
}
}
private void writeArchive(CopyActionProcessingStream stream) throws IOException {
OutputStream outputStream = new FileOutputStream(this.output);
private void writeArchive(CopyActionProcessingStream copyActions) throws IOException {
OutputStream output = new FileOutputStream(this.output);
try {
writeLaunchScriptIfNecessary(outputStream);
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);
}
writeArchive(copyActions, output);
}
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) {
try {
outputStream.close();
@ -144,17 +175,20 @@ class BootZipCopyAction implements CopyAction {
*/
private class Processor {
private ZipArchiveOutputStream outputStream;
private ZipArchiveOutputStream out;
private Spec<FileTreeElement> writtenLoaderEntries;
Processor(ZipArchiveOutputStream outputStream) {
this.outputStream = outputStream;
private Set<String> writtenDirectories = new LinkedHashSet<>();
private Set<String> writtenLibraries = new LinkedHashSet<>();
Processor(ZipArchiveOutputStream out) {
this.out = out;
}
void process(FileCopyDetails details) {
if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details)
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) {
if (skipProcessing(details)) {
return;
}
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 {
writeLoaderEntriesIfNecessary(null);
writeJarToolsIfNecessary();
writeLayersIndexIfNecessary();
writeClassPathIndexIfNecessary();
}
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)
return;
}
LoaderZipEntries loaderEntries = new LoaderZipEntries(
LoaderZipEntries entries = new LoaderZipEntries(
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) {
@ -196,40 +312,47 @@ class BootZipCopyAction implements CopyAction {
return segments.length > 0 && "META-INF".equals(segments[0]);
}
private void processDirectory(FileCopyDetails details) throws IOException {
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/');
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
this.outputStream.putArchiveEntry(archiveEntry);
this.outputStream.closeArchiveEntry();
private void writeJarToolsIfNecessary() throws IOException {
if (BootZipCopyAction.this.layerResolver == null || !BootZipCopyAction.this.includeLayerTools) {
return;
}
writeJarModeLibrary(JarModeLibrary.LAYER_TOOLS);
}
private void processFile(FileCopyDetails details) throws IOException {
String relativePath = details.getRelativePath().getPathString();
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath);
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, archiveEntry);
private void writeJarModeLibrary(JarModeLibrary jarModeLibrary) throws IOException {
String name = BootZipCopyAction.this.layerResolver.getPath(jarModeLibrary);
writeFile(name, ZipEntryWriter.fromInputStream(jarModeLibrary.openStream()));
}
private void writeLayersIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
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 {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
Crc32OutputStream crcStream = new Crc32OutputStream();
details.copyTo(crcStream);
archiveEntry.setCrc(crcStream.getCrc());
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
private void writeClassPathIndexIfNecessary() throws IOException {
Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes();
String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index");
if (classPathIndex != null) {
writeFile(classPathIndex,
ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, this.writtenLibraries));
}
}
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) {
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified()
: 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.
*/

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

@ -25,11 +25,14 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.jar.JarFile;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat;
/**
@ -75,8 +78,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
writeResource();
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])) {
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"))
.isNotNull();
assertThat(jarFile.getEntry(jarModeLayerTools())).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"))
.isNotNull();
@ -89,10 +91,11 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
void customLayers() throws IOException {
writeMainClass();
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])) {
assertThat(jarFile.getEntry("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"))
.isNotNull();
assertThat(jarFile.getEntry(jarModeLayerTools())).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"))
.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() {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs();

@ -20,9 +20,8 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;
@ -31,16 +30,15 @@ import java.util.zip.ZipEntry;
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.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.junit.jupiter.api.Test;
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 org.springframework.boot.gradle.tasks.bundling.BootJarTests.TestBootJar;
import org.springframework.boot.loader.tools.JarModeLibrary;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ -53,10 +51,10 @@ import static org.mockito.Mockito.mock;
* @author Madhura Bhave
* @author Scott Frederick
*/
class BootJarTests extends AbstractBootArchiveTests<BootJar> {
class BootJarTests extends AbstractBootArchiveTests<TestBootJar> {
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
@ -121,13 +119,17 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test
void whenJarIsLayeredWithCustomStrategiesThenContentsAreMovedToLayerDirectories() throws IOException {
File jar = createLayeredJar((configuration) -> {
configuration.layersOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application");
configuration.libraries(createLibraryStrategy("my-snapshot-deps", "com.example:*:*.SNAPSHOT"),
createLibraryStrategy("my-internal-deps", "com.example:*:*"),
createLibraryStrategy("my-deps", "*:*"));
configuration.application(createResourceStrategy("resources", "static/**"),
createResourceStrategy("application", "**"));
File jar = createLayeredJar((layered) -> {
layered.application((application) -> {
application.intoLayer("resources", (spec) -> spec.include("static/**"));
application.intoLayer("application");
});
layered.dependencies((dependencies) -> {
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);
assertThat(entryNames)
@ -139,16 +141,6 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
.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
void whenJarIsLayeredJarsInLibAreStored() throws IOException {
try (JarFile jarFile = new JarFile(createLayeredJar())) {
@ -172,7 +164,14 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
@Test
void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
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
@ -198,24 +197,24 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
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) {
getTask().layers(action);
getTask().layered(action);
}
else {
getTask().layers();
getTask().layered();
}
addContent();
executeTask();
return getTask().getArchiveFile().get().getAsFile();
}
private File createLayeredJar() throws IOException {
return createLayeredJar(null);
}
private void addContent() throws IOException {
BootJar bootJar = getTask();
TestBootJar bootJar = getTask();
bootJar.setMainClassName("com.example.Main");
File classesJavaMain = new File(this.temp, "classes/java/main");
File applicationClass = new File(classesJavaMain, "com/example/Application.class");
@ -231,32 +230,33 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
css.createNewFile();
bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"),
jarFile("third-library-SNAPSHOT.jar"));
Set<ResolvedArtifactResult> resolvedArtifacts = new HashSet<>();
resolvedArtifacts.add(mockLibraryArtifact("first-library.jar", "com.example:first-library:1.0.0"));
resolvedArtifacts.add(mockLibraryArtifact("second-library.jar", "com.example:second-library:1.0.0"));
resolvedArtifacts
.add(mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example:third-library:1.0.0.SNAPSHOT"));
ArtifactCollection artifacts = mock(ArtifactCollection.class);
given(artifacts.getArtifacts()).willReturn(resolvedArtifacts);
ResolvableDependencies deps = mock(ResolvableDependencies.class);
given(deps.getArtifacts()).willReturn(artifacts);
bootJar.resolvedDependencies(deps);
Set<ResolvedArtifactResult> artifacts = new LinkedHashSet<>();
artifacts.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"));
artifacts.add(
mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT"));
ArtifactCollection resolvedDependencies = mock(ArtifactCollection.class);
given(resolvedDependencies.getArtifacts()).willReturn(artifacts);
ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class);
given(resolvableDependencies.getArtifacts()).willReturn(resolvedDependencies);
Configuration configuration = mock(Configuration.class);
given(configuration.isCanBeResolved()).willReturn(true);
given(configuration.getIncoming()).willReturn(resolvableDependencies);
bootJar.setConfiguration(Collections.singleton(configuration));
}
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String coordinates) {
ComponentIdentifier libraryId = mock(ComponentIdentifier.class);
given(libraryId.getDisplayName()).willReturn(coordinates);
private ResolvedArtifactResult mockLibraryArtifact(String fileName, String group, String module, String version) {
ModuleComponentIdentifier identifier = mock(ModuleComponentIdentifier.class);
given(identifier.getGroup()).willReturn(group);
given(identifier.getModule()).willReturn(module);
given(identifier.getVersion()).willReturn(version);
ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class);
given(libraryArtifactId.getComponentIdentifier()).willReturn(libraryId);
given(libraryArtifactId.getComponentIdentifier()).willReturn(identifier);
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);
return libraryArtifact;
}
@ -272,4 +272,19 @@ class BootJarTests extends AbstractBootArchiveTests<BootJar> {
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 {
mainClassName = 'com.example.Application'
layers {
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 "*:*" } }
}
layered {
application {
layerContent("static") {
locations {
include "META-INF/resources/**", "resources/**"
include "static/**", "public/**"
}
intoLayer("static") {
include "META-INF/resources/**", "resources/**", "static/**", "public/**"
}
intoLayer("app")
}
dependencies {
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
layerContent("app") {
locations {
include "**"
}
intoLayer("commons-dependencies") {
include "org.apache.commons:*"
}
intoLayer("dependencies")
}
layerOrder "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"
}
}

@ -11,7 +11,7 @@ bootJar {
}
}
if (project.hasProperty('layered') && project.getProperty('layered')) {
layers {
layered {
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 org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link Library} implementation for internal jarmode jars.
*
* @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) {
super(name, null, LibraryScope.RUNTIME, false);
JarModeLibrary(String artifactId) {
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
InputStream openStream() throws IOException {
String path = "META-INF/jarmode/" + getName();
public InputStream openStream() throws IOException {
String path = "META-INF/jarmode/" + getCoordinates().getArtifactId() + ".jar";
URL resource = getClass().getClassLoader().getResource(path);
Assert.state(resource != null, "Unable to find resource " + path);
return resource.openStream();

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

@ -17,6 +17,7 @@
package org.springframework.boot.loader.tools;
import java.util.Iterator;
import java.util.stream.Stream;
/**
* 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
* least frequently changed layer).
* @return the layers iterator
*/
@Override
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.
* @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})
*/
Layer getLayer(String resourceName);
Layer getLayer(String applicationResource);
/**
* Return the layer that contains the given library.

@ -16,82 +16,60 @@
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 Phillip Webb
* @since 2.3.0
*/
public final class 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;
}
public interface LibraryCoordinates {
/**
* Return the group ID of the coordinates.
* @return the group ID
*/
public String getGroupId() {
return this.groupId;
}
String getGroupId();
/**
* Return the artifact ID of the coordinates.
* @return the artifact ID
*/
public String getArtifactId() {
return this.artifactId;
}
String getArtifactId();
/**
* Return the version of the coordinates.
* @return the version
*/
public String getVersion() {
return this.version;
String getVersion();
/**
* 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
public String toString() {
static String toStandardNotationString(LibraryCoordinates coordinates) {
if (coordinates == null) {
return "::";
}
StringBuilder builder = new StringBuilder();
builder.append((this.groupId != null) ? this.groupId : "");
builder.append((coordinates.getGroupId() != null) ? coordinates.getGroupId() : "");
builder.append(":");
builder.append((this.artifactId != null) ? this.artifactId : "");
builder.append((coordinates.getArtifactId() != null) ? coordinates.getArtifactId() : "");
builder.append(":");
builder.append((this.version != null) ? this.version : "");
builder.append((coordinates.getVersion() != null) ? coordinates.getVersion() : "");
return builder.toString();
}

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
/**
* 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();
}
@Override
public Stream<Layer> stream() {
return LAYERS.stream();
}
}

@ -14,29 +14,33 @@
* limitations under the License.
*/
package org.springframework.boot.loader.tools.layer.application;
import java.util.List;
package org.springframework.boot.loader.tools.layer;
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 Phillip Webb
* @since 2.3.0
*/
public class LocationFilter extends AbstractResourceFilter {
public class ApplicationContentFilter implements ContentFilter<String> {
private static final AntPathMatcher MATCHER = new AntPathMatcher();
public LocationFilter(List<String> includes, List<String> excludes) {
super(includes, excludes);
private final String pattern;
public ApplicationContentFilter(String pattern) {
Assert.hasText(pattern, "Pattern must not be empty");
this.pattern = pattern;
}
@Override
protected boolean isMatch(String resourceName, List<String> toMatch) {
return toMatch.stream().anyMatch((pattern) -> MATCHER.match(pattern, resourceName));
public boolean matches(String path) {
return MATCHER.match(this.pattern, path);
}
}

@ -14,8 +14,24 @@
* 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.
*/
package org.springframework.boot.loader.tools.layer.library;
import java.io.Serializable;
package 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 Phillip Webb
* @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
* found, {@code null} is returned.
* @param library the library
* @return the matching layer or {@code null}
* Returns {@code true} if the specified item is contained in this selection.
* @param item the item to test
* @return if the item is contained
*/
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.Iterator;
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.Layers;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.layer.application.ResourceStrategy;
import org.springframework.boot.loader.tools.layer.library.LibraryStrategy;
import org.springframework.util.Assert;
/**
* Implementation of {@link Layers} representing user-provided layers.
* Custom {@link Layers} implementation where layer content is selected by the user.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.3.0
*/
public class CustomLayers implements 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,
List<LibraryStrategy> libraryStrategies) {
public CustomLayers(List<Layer> layers, List<ContentSelector<String>> applicationSelectors,
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.resourceStrategies = new ArrayList<>(resourceStrategies);
this.libraryStrategies = new ArrayList<>(libraryStrategies);
this.applicationSelectors = new ArrayList<>(applicationSelectors);
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
@ -52,35 +72,28 @@ public class CustomLayers implements Layers {
return this.layers.iterator();
}
@Override
public Stream<Layer> stream() {
return this.layers.stream();
}
@Override
public Layer getLayer(String resourceName) {
for (ResourceStrategy strategy : this.resourceStrategies) {
Layer matchingLayer = strategy.getMatchingLayer(resourceName);
if (matchingLayer != null) {
validateLayerName(matchingLayer, "Resource '" + resourceName + "'");
return matchingLayer;
}
}
throw new IllegalStateException("Resource '" + resourceName + "' did not match any layer.");
return selectLayer(resourceName, this.applicationSelectors, () -> "Resource '" + resourceName + "'");
}
@Override
public Layer getLayer(Library library) {
for (LibraryStrategy strategy : this.libraryStrategies) {
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.");
return selectLayer(library, this.librarySelectors, () -> "Library '" + library.getName() + "'");
}
private void validateLayerName(Layer layer, String nameText) {
if (!this.layers.contains(layer)) {
throw new IllegalStateException(nameText + " matched a layer '" + layer
+ "' that is not included in the configured layers " + this.layers + ".");
private <T> Layer selectLayer(T item, List<ContentSelector<T>> selectors, Supplier<String> name) {
for (ContentSelector<T> selector : selectors) {
if (selector.contains(item)) {
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.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -650,6 +651,11 @@ abstract class AbstractPackagerTests<P extends Packager> {
return this.layers.iterator();
}
@Override
public Stream<Layer> stream() {
return this.layers.stream();
}
@Override
public Layer getLayer(String name) {
return DEFAULT_LAYER;

@ -19,56 +19,42 @@ package org.springframework.boot.loader.tools;
import org.junit.jupiter.api.Test;
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}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class LibraryCoordinatesTests {
@Test
void parseCoordinatesWithAllElements() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0");
assertThat(coordinates.getGroupId()).isEqualTo("com.acme");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isEqualTo("1.0.0");
void ofCreateLibraryCoordinates() {
LibraryCoordinates coordinates = LibraryCoordinates.of("g", "a", "v");
assertThat(coordinates.getGroupId()).isEqualTo("g");
assertThat(coordinates.getArtifactId()).isEqualTo("a");
assertThat(coordinates.getVersion()).isEqualTo("v");
assertThat(coordinates.toString()).isEqualTo("g:a:v");
}
@Test
void parseCoordinatesWithoutVersion() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library");
assertThat(coordinates.getGroupId()).isEqualTo("com.acme");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isNull();
void toStandardNotationStringWhenCoordinatesAreNull() {
assertThat(LibraryCoordinates.toStandardNotationString(null)).isEqualTo("::");
}
@Test
void parseCoordinatesWithEmptyElements() {
LibraryCoordinates coordinates = new LibraryCoordinates(":my-library:");
assertThat(coordinates.getGroupId()).isEqualTo("");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isNull();
void toStandardNotationStringWhenCoordinatesElementsNull() {
assertThat(LibraryCoordinates.toStandardNotationString(mock(LibraryCoordinates.class))).isEqualTo("::");
}
@Test
void parseCoordinatesWithExtraElements() {
LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0.BUILD-SNAPSHOT:11111");
assertThat(coordinates.getGroupId()).isEqualTo("com.acme");
assertThat(coordinates.getArtifactId()).isEqualTo("my-library");
assertThat(coordinates.getVersion()).isEqualTo("1.0.0.BUILD-SNAPSHOT");
}
@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:");
void toStandardNotationString() {
LibraryCoordinates coordinates = mock(LibraryCoordinates.class);
given(coordinates.getGroupId()).willReturn("a");
given(coordinates.getArtifactId()).willReturn("b");
given(coordinates.getVersion()).willReturn("c");
assertThat(LibraryCoordinates.toStandardNotationString(coordinates)).isEqualTo("a:b:c");
}
}

@ -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.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.util.FileSystemUtils;
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/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.hasEntryWithNameStartingWith(
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
.hasEntryWithNameStartingWith(jarModeLayerTools());
});
}
@ -308,8 +308,7 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/")
.hasEntryWithNameStartingWith("BOOT-INF/layers/dependencies/lib/jar-release")
.hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot")
.doesNotHaveEntryWithNameStartingWith(
"BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
.doesNotHaveEntryWithNameStartingWith(jarModeLayerTools());
});
}
@ -333,6 +332,13 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests {
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) {
AtomicReference<String> jarHash = new AtomicReference<>();
mavenBuild.project("jar-output-timestamp").execute((project) -> {

@ -1,35 +1,23 @@
<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/layers/layers-configuration-2.3.xsd">
<layers>
<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/layers/layers-2.3.xsd">
<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>application</layer>
<layer>snapshot-dependencies</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<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>
</layerOrder>
</layers>

@ -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.Libraries;
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}.
@ -135,15 +136,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
}
if (this.layers != null && this.layers.isEnabled()) {
if (this.layers.getConfiguration() != null) {
try {
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.setLayers(getCustomLayers(this.layers.getConfiguration()));
}
packager.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools());
@ -151,8 +144,19 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
return packager;
}
private Document getDocumentIfAvailable(File configurationFile) throws Exception {
InputSource inputSource = new InputSource(new FileInputStream(configurationFile));
private CustomLayers getCustomLayers(File configuration) {
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();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(inputSource);

@ -80,8 +80,7 @@ public class ArtifactsLibraries implements Libraries {
name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name);
}
LibraryCoordinates coordinates = new LibraryCoordinates(artifact.getGroupId(), artifact.getArtifactId(),
artifact.getVersion());
LibraryCoordinates coordinates = new ArtifactLibraryCoordinates(artifact);
callback.library(new Library(name, artifact.getFile(), scope, coordinates, isUnpackRequired(artifact)));
}
}
@ -122,4 +121,37 @@ public class ArtifactsLibraries implements Libraries {
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;
import java.util.ArrayList;
import java.util.Collections;
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.Element;
@ -26,113 +28,76 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
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.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;
import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector;
import org.springframework.boot.loader.tools.layer.LibraryContentFilter;
/**
* Produces a {@link CustomLayers} based on the given {@link Document}.
*
* @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();
NodeList nodes = root.getChildNodes();
List<Layer> layers = new ArrayList<>();
List<LibraryStrategy> libraryStrategies = new ArrayList<>();
List<ResourceStrategy> resourceStrategies = new ArrayList<>();
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);
List<ContentSelector<String>> applicationSelectors = getApplicationSelectors(root);
List<ContentSelector<Library>> librarySelectors = getLibrarySelectors(root);
List<Layer> layers = getLayers(root);
return new CustomLayers(layers, applicationSelectors, librarySelectors);
}
private void processNode(List<Layer> layers, List<LibraryStrategy> libraryStrategies,
List<ResourceStrategy> resourceStrategies, Element node) {
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<ContentSelector<String>> getApplicationSelectors(Element root) {
return getSelectors(root, "application", ApplicationContentFilter::new);
}
private List<Layer> getLayers(Element element) {
List<Layer> layers = new ArrayList<>();
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 List<ContentSelector<Library>> getLibrarySelectors(Element root) {
return getSelectors(root, "dependencies", LibraryContentFilter::new);
}
private <T, E> List<T> getStrategies(NodeList nodes, StrategyFactory<E, T> strategyFactory,
FilterFactory<E> filterFactory, Predicate<String> filterPredicate) {
List<T> contents = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
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));
}
}
private List<Layer> getLayers(Element root) {
Element layerOrder = getChildElement(root, "layerOrder");
if (layerOrder == null) {
return Collections.emptyList();
}
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) {
NodeList childNodes = node.getChildNodes();
Assert.state(childNodes.getLength() > 0, "Filters for layer-content must not be empty.");
List<E> filters = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node childNode = childNodes.item(i);
if (childNode instanceof Element) {
List<String> include = getPatterns((Element) childNode, "include");
List<String> exclude = getPatterns((Element) childNode, "exclude");
if (predicate.test(childNode.getNodeName())) {
filters.add(factory.getFilter(include, exclude));
}
private <T> List<ContentSelector<T>> getSelectors(Element root, String elementName,
Function<String, ContentFilter<T>> filterFactory) {
Element element = getChildElement(root, elementName);
if (element == null) {
return Collections.emptyList();
}
List<ContentSelector<T>> selectors = new ArrayList<>();
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
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<>();
NodeList nodes = element.getElementsByTagName(key);
for (int j = 0; j < nodes.getLength(); j++) {
Node node = nodes.item(j);
NodeList nodes = element.getElementsByTagName(tagName);
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node instanceof Element) {
patterns.add(node.getTextContent());
}
@ -140,16 +105,15 @@ public class CustomLayersProvider {
return patterns;
}
interface StrategyFactory<E, T> {
T getStrategy(String layer, List<E> filters);
}
interface FilterFactory<E> {
E getFilter(List<String> includes, List<String> excludes);
private Element getChildElement(Element element, String tagName) {
NodeList nodes = element.getElementsByTagName(tagName);
if (nodes.getLength() == 0) {
return null;
}
if (nodes.getLength() > 1) {
throw new IllegalStateException("Multiple '" + tagName + "' nodes found");
}
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
void getLayerResolverWhenDocumentValid() throws Exception {
CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml"));
assertThat(layers).extracting("name").containsExactly("configuration", "application", "my-resources",
"snapshot-dependencies", "my-deps", "my-dependencies-name");
assertThat(layers).extracting("name").containsExactly("my-deps", "my-dependencies-name",
"snapshot-dependencies", "my-resources", "configuration", "application");
Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT");
Library groupId = mockLibrary("my-library", "com.acme", 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) {
Library library = mock(Library.class);
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;
}
@Test
void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() {
assertThatIllegalStateException()
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("library-layer-no-filter.xml")))
.withMessage("Filters for layer-content must not be empty.");
void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() throws Exception {
CustomLayers layers = this.customLayersProvider.getLayers(getDocument("dependencies-layer-no-filter.xml"));
Library library = mockLibrary("my-library", "com.acme", null);
assertThat(layers.getLayer(library).toString()).isEqualTo("my-deps");
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer("application.yml"))
.withMessageContaining("match any layer");
}
@Test
void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() {
assertThatIllegalStateException()
.isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("resource-layer-no-filter.xml")))
.withMessage("Filters for layer-content must not be empty.");
void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() throws Exception {
CustomLayers layers = this.customLayersProvider.getLayers(getDocument("application-layer-no-filter.xml"));
Library library = mockLibrary("my-library", "com.acme", null);
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 {

@ -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"
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>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>
<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">
<application>
<layer-content layer="my-resources">
<locations>
<include>META-INF/resources/**</include>
</locations>
</layer-content>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
<into layer="my-resources">
<include>META-INF/resources/**</include>
<exclude>*.properties</exclude>
</into>
<into layer="configuration">
<include>**/application*.*</include>
</into>
<into layer="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"
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-layer</layer>
</layers>
<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">
<application>
<layer-content layer="my-layer"/>
<into layer="my-layer" />
</application>
</layers-configuration>
<layerOrder>
<layer>my-layer</layer>
</layerOrder>
</layers>
Loading…
Cancel
Save