From 7bc7d86ad4a2aae902f032494f9756012cfe94b6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 24 Mar 2020 17:00:39 -0700 Subject: [PATCH] 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 --- .../packaging/boot-jar-layered-custom.gradle | 28 +- .../boot-jar-layered-custom.gradle.kts | 28 +- .../gradle/packaging/boot-jar-layered.gradle | 2 +- .../packaging/boot-jar-layered.gradle.kts | 2 +- .../boot/gradle/plugin/JavaPluginAction.java | 4 - .../tasks/bundling/BootArchiveSupport.java | 93 ++++-- .../boot/gradle/tasks/bundling/BootJar.java | 296 ++++++------------ .../boot/gradle/tasks/bundling/BootWar.java | 86 +++-- .../tasks/bundling/BootZipCopyAction.java | 293 +++++++++++++---- .../tasks/bundling/LayerConfiguration.java | 237 -------------- .../gradle/tasks/bundling/LayerResolver.java | 197 ++++++++++++ .../gradle/tasks/bundling/LayeredSpec.java | 228 ++++++++++++++ .../tasks/bundling/LoaderZipEntries.java | 6 +- .../bundling/BootJarIntegrationTests.java | 20 +- .../gradle/tasks/bundling/BootJarTests.java | 127 ++++---- ...ootJarIntegrationTests-customLayers.gradle | 31 +- ...tJarIntegrationTests-implicitLayers.gradle | 2 +- .../bundling/BootJarIntegrationTests.gradle | 2 +- .../tools/DefaultLibraryCoordinates.java | 79 +++++ .../boot/loader/tools/JarModeLibrary.java | 37 ++- .../boot/loader/tools/Layer.java | 3 +- .../boot/loader/tools/Layers.java | 14 +- .../boot/loader/tools/LibraryCoordinates.java | 76 ++--- .../boot/loader/tools/StandardLayers.java | 6 + ...ter.java => ApplicationContentFilter.java} | 22 +- .../package-info.java => ContentFilter.java} | 20 +- ...raryStrategy.java => ContentSelector.java} | 27 +- .../boot/loader/tools/layer/CustomLayers.java | 71 +++-- .../layer/IncludeExcludeContentSelector.java | 96 ++++++ .../tools/layer/LibraryContentFilter.java | 61 ++++ .../application/AbstractResourceFilter.java | 51 --- .../application/FilteredResourceStrategy.java | 61 ---- .../layer/application/ResourceFilter.java | 43 --- .../layer/application/ResourceStrategy.java | 39 --- .../tools/layer/library/CoordinateFilter.java | 84 ----- .../library/FilteredLibraryStrategy.java | 62 ---- .../tools/layer/library/LibraryFilter.java | 45 --- .../tools/layer/library/package-info.java | 21 -- .../loader/tools/AbstractPackagerTests.java | 6 + .../loader/tools/LibraryCoordinatesTests.java | 52 ++- .../layer/ApplicationContentFilterTests.java | 57 ++++ .../loader/tools/layer/CustomLayersTests.java | 126 -------- .../IncludeExcludeContentSelectorTests.java | 141 +++++++++ .../layer/LibraryContentFilterTests.java | 108 +++++++ .../FilteredResourceStrategyTests.java | 104 ------ .../application/LocationFilterTests.java | 57 ---- .../layer/library/CoordinateFilterTests.java | 111 ------- .../library/FilteredLibraryStrategyTests.java | 119 ------- .../boot/maven/JarIntegrationTests.java | 14 +- .../jar-layered-custom/jar/src/layers.xml | 50 ++- .../boot/maven/AbstractPackagerMojo.java | 26 +- .../boot/maven/ArtifactsLibraries.java | 36 ++- .../boot/maven/CustomLayersProvider.java | 168 ++++------ .../src/main/xsd/layers-2.3.xsd | 78 +++++ .../src/main/xsd/layers-configuration-2.3.xsd | 96 ------ .../boot/maven/CustomLayersProviderTests.java | 26 +- .../resources/application-layer-no-filter.xml | 11 + .../dependencies-layer-no-filter.xml | 11 + .../src/test/resources/layers.xml | 75 ++--- .../resources/library-layer-no-filter.xml | 11 - .../resources/resource-layer-no-filter.xml | 18 +- 61 files changed, 2047 insertions(+), 2054 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLibraryCoordinates.java rename spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/{application/LocationFilter.java => ApplicationContentFilter.java} (60%) rename spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/{application/package-info.java => ContentFilter.java} (59%) rename spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/{library/LibraryStrategy.java => ContentSelector.java} (57%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/AbstractResourceFilter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategy.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceFilter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceStrategy.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategyTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/LocationFilterTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration-2.3.xsd create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle index 11e33867bf..2fb89c7d5f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle @@ -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[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts index c4a3bf7747..a438e395a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts @@ -7,27 +7,17 @@ plugins { // tag::layered[] tasks.getByName("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[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle index 4ec810698f..502f87f4c6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle @@ -9,6 +9,6 @@ bootJar { // tag::layered[] bootJar { - layers() + layered() } // end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts index a7c5a41482..2d6735740a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered.gradle.kts @@ -11,6 +11,6 @@ tasks.getByName("bootJar") { // tag::layered[] tasks.getByName("bootJar") { - layers() + layered() } // end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index 324e8af53b..46021de03d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -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) mainSourceSet::getRuntimeClasspath); - Configuration runtimeClasspathConfiguration = project.getConfigurations() - .getByName(mainSourceSet.getRuntimeClasspathConfigurationName()); - runtimeClasspathConfiguration.getIncoming().afterResolve(bootJar::resolvedDependencies); bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath)); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index fd9db76e33..5cda8cf720 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -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 compressionResolver; - private final PatternSet exclusions = new PatternSet(); private final String loaderMainClass; + private final Spec librarySpec; + + private final Function compressionResolver; + private LaunchScriptConfiguration launchScript; private boolean excludeDevtools = true; - BootArchiveSupport(String loaderMainClass, Function compressionResolver) { + BootArchiveSupport(String loaderMainClass, Spec librarySpec, + Function 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 requiresUnpack = this.requiresUnpack.getAsSpec(); + Spec exclusions = this.exclusions.getAsExcludeSpec(); + LaunchScriptConfiguration launchScript = this.launchScript; + Spec librarySpec = this.librarySpec; + Function 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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 11a336de9d..2ea8349fb3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -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 static final String LIB_FOLDER = "BOOT-INF/lib/"; + + private static final String LAYERS_INDEX = "BOOT-INF/layers.idx"; + + private static final String CLASSPATH_INDEX = "BOOT-INF/classpath.idx"; + + private final BootArchiveSupport support; + + private final CopySpec bootInfSpec; private String mainClassName; private FileCollection classpath; - private Layers layers; - - private LayerConfiguration layerConfiguration; - - private static final String BOOT_INF_LAYERS = "BOOT-INF/layers"; - - private final List dependencies = new ArrayList<>(); - - private final Map coordinatesByFileName = new HashMap<>(); + 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) () -> createClasspathIndex(BootJar.this.dependencies))); - this.bootInf.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(); - } - }); + this.support = new BootArchiveSupport(LAUNCHER, this::isLibrary, this::resolveZipCompression); + this.bootInfSpec = getProject().copySpec().into("BOOT-INF"); + configureBootInfSpec(this.bootInfSpec); + getMainSpec().with(this.bootInfSpec); } - private Action classpathFiles(Spec filter) { - return (copySpec) -> copySpec.from((Callable>) () -> (this.classpath != null) - ? this.classpath.filter(filter) : Collections.emptyList()); + 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())); + } + + private Iterable classpathDirectories() { + return classpathEntries(File::isDirectory); + } + + private Iterable classpathFiles() { + return classpathEntries(File::isFile); + } + + private Iterable classpathEntries(Spec 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 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 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; + @Optional + public LayeredSpec getLayered() { + return this.layered; } - /** - * Configures the archive to have layers. - */ - public void layers() { - enableLayers(); + public void layered() { + layered(true); } - public void layers(Action action) { - action.execute(enableLayers()); + public void layered(boolean layered) { + this.layered = layered ? new LayeredSpec() : null; } - private LayerConfiguration enableLayers() { - if (this.layerConfiguration == null) { - this.layerConfiguration = new LayerConfiguration(); - } - - return this.layerConfiguration; + public void layered(Closure closure) { + layered(ConfigureUtil.configureUsing(closure)); } - private void applyLayers() { - if (this.layerConfiguration == null) { - return; - } - - if (this.layerConfiguration.getLayersOrder() == null || this.layerConfiguration.getLayersOrder().isEmpty()) { - this.layers = Layers.IMPLICIT; - } - else { - List 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) () -> { - String jarName = "spring-boot-jarmode-layertools.jar"; - InputStream stream = getClass().getClassLoader().getResourceAsStream("META-INF/jarmode/" + jarName); - File taskTmp = new File(getProject().getBuildDir(), "tmp/" + getName()); - taskTmp.mkdirs(); - File layerToolsJar = new File(taskTmp, jarName); - FileCopyUtils.copy(stream, new FileOutputStream(layerToolsJar)); - return layerToolsJar; - })); - } - this.bootInf.eachFile((details) -> { - Layer layer = layerForFileDetails(details); - if (layer != null) { - String relativePath = details.getPath().substring("BOOT-INF/".length()); - details.setPath(BOOT_INF_LAYERS + "/" + layer + "/" + relativePath); - } - }).setIncludeEmptyDirs(false); - this.bootInf.into("", (spec) -> spec.from(createLayersIndex())); - } - - private Layer layerForFileDetails(FileCopyDetails details) { - 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; - } - - 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); - } - } - - private void resolveCoordinatesForFiles(ResolvableDependencies resolvableDependencies) { - ArtifactCollection resolvedArtifactResults = resolvableDependencies.getArtifacts(); - Set artifacts = resolvedArtifactResults.getArtifacts(); - artifacts.forEach((artifact) -> this.coordinatesByFileName.put(artifact.getFile().getName(), - artifact.getId().getComponentIdentifier().getDisplayName())); - } - - @Input - boolean isLayered() { - return this.layerConfiguration != null; + public void layered(Action 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. - *

- * By default, any file in {@code BOOT-INF/lib/} or - * {@code BOOT-INF/layers//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 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 the result type + * @param callable the callable + * @return an action to add the callable to the spec + */ + private static Action fromCallTo(Callable callable) { + return (spec) -> spec.from(callTo(callable)); + } + + /** + * Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return the callable + */ + private static Callable callTo(Callable callable) { + return callable; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index de30351b27..61384000bf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -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>) () -> (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. - *

- * 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 the result type + * @param callable the callable + * @return an action to add the callable to the spec + */ + private static Action fromCallTo(Callable callable) { + return (spec) -> spec.from(callTo(callable)); + } + + /** + * Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return the callable + */ + private static Callable callTo(Callable callable) { + return callable; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 6528ae7b5c..0267e6f81b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -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 requiresUnpack; private final Spec exclusions; private final LaunchScriptConfiguration launchScript; + private final Spec librarySpec; + private final Function compressionResolver; private final String encoding; - BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader, - Spec requiresUnpack, Spec exclusions, - LaunchScriptConfiguration launchScript, Function compressionResolver, - String encoding) { + private final LayerResolver layerResolver; + + BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader, + boolean includeLayerTools, Spec requiresUnpack, Spec exclusions, + LaunchScriptConfiguration launchScript, Spec librarySpec, + Function 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 writtenLoaderEntries; - Processor(ZipArchiveOutputStream outputStream) { - this.outputStream = outputStream; + private Set writtenDirectories = new LinkedHashSet<>(); + + private Set 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); - } - this.outputStream.putArchiveEntry(archiveEntry); - details.copyTo(this.outputStream); - this.outputStream.closeArchiveEntry(); + private void writeJarModeLibrary(JarModeLibrary jarModeLibrary) throws IOException { + String name = BootZipCopyAction.this.layerResolver.getPath(jarModeLibrary); + writeFile(name, ZipEntryWriter.fromInputStream(jarModeLibrary.openStream())); } - 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 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())); } } + 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 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. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java deleted file mode 100644 index 3c49c3c85a..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerConfiguration.java +++ /dev/null @@ -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 layersOrder = new ArrayList<>(); - - private List resourceStrategies = new ArrayList<>(); - - private List 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 getLayersOrder() { - return this.layersOrder; - } - - public void layersOrder(String... layers) { - this.layersOrder = Arrays.asList(layers); - } - - public void layersOrder(List layers) { - this.layersOrder = layers; - } - - @Input - public List getApplication() { - return this.resourceStrategies; - } - - public void application(ResourceStrategy... resourceStrategies) { - assertLayersOrderConfigured(); - this.resourceStrategies = Arrays.asList(resourceStrategies); - } - - public void application(Action config) { - assertLayersOrderConfigured(); - this.strategySpec = StrategySpec.forResources(); - config.execute(this); - } - - @Input - public List getLibraries() { - return this.libraryStrategies; - } - - public void libraries(LibraryStrategy... strategies) { - assertLayersOrderConfigured(); - this.libraryStrategies = Arrays.asList(strategies); - } - - public void libraries(Action 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 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 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 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 libraryFilters; - - private List resourceFilters; - - private List filterIncludes; - - private List 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 libraryFilters() { - return this.libraryFilters; - } - - private void addLibraryFilter(LibraryFilter filter) { - this.libraryFilters.add(filter); - } - - private List resourceFilters() { - return this.resourceFilters; - } - - private void addResourceFilter(ResourceFilter filter) { - this.resourceFilters.add(filter); - } - - private List 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 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; - - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java new file mode 100644 index 0000000000..cd6cf66b99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java @@ -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 librarySpec; + + LayerResolver(Iterable configurations, LayeredSpec layeredConfiguration, + Spec 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 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 configurationDependencies = new LinkedHashMap<>(); + + ResolvedDependencies(Iterable 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 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(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java new file mode 100644 index 0000000000..c3c1c41bbc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java @@ -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 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 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 action) { + action.execute(this.dependencies); + } + + @Input + public List getLayerOrder() { + return this.layerOrder; + } + + public void layerOrder(String... layerOrder) { + this.layerOrder = Arrays.asList(layerOrder); + } + + public void layerOrder(List 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 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 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 action) { + IntoLayerSpec spec = new IntoLayerSpec(layer); + action.execute(spec); + this.intoLayers.add(spec); + } + + List> asSelectors(Function> 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 includes = new ArrayList<>(); + + private final List 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)); + } + + ContentSelector asSelector(Function> 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> asSelectors() { + return asSelectors(ApplicationContentFilter::new); + } + + } + + public static class DependenciesSpec extends IntoLayersSpec { + + public DependenciesSpec(IntoLayerSpec... contents) { + super(contents); + } + + List> asSelectors() { + return asSelectors(LibraryContentFilter::new); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index a4bce45e02..a6fcd39612 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -44,18 +44,18 @@ class LoaderZipEntries { this.entryTime = entryTime; } - Spec writeTo(ZipArchiveOutputStream zipOutputStream) throws IOException { + Spec 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(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index a6a63dabb7..a8e7b6eeeb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -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(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index aa70e1eee9..562f849e7c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -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 { +class BootJarTests extends AbstractBootArchiveTests { 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 { @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 entryNames = getEntryNames(jar); assertThat(entryNames) @@ -139,16 +141,6 @@ class BootJarTests extends AbstractBootArchiveTests { .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 { @Test void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { List 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 { return getTask().getArchiveFile().get().getAsFile(); } - private File createLayeredJar(Action action) throws IOException { + private File createLayeredJar() throws IOException { + return createLayeredJar(null); + } + + private File createLayeredJar(Action 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 { css.createNewFile(); bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), jarFile("third-library-SNAPSHOT.jar")); - - Set 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 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 { getTask().copy(); } + public static class TestBootJar extends BootJar { + + private Iterable configurations = Collections.emptySet(); + + @Override + protected Iterable getConfigurations() { + return this.configurations; + } + + void setConfiguration(Iterable configurations) { + this.configurations = configurations; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle index a516b29765..f3bec81b59 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -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/**" - } - } - layerContent("app") { - locations { - include "**" - } + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" } + intoLayer("app") } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder "dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle index 0d12faa1f1..1cd9de76bf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -5,7 +5,7 @@ plugins { bootJar { mainClassName = 'com.example.Application' - layers() + layered() } repositories { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle index 8ad873d218..fb56a68e55 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle @@ -11,7 +11,7 @@ bootJar { } } if (project.hasProperty('layered') && project.getProperty('layered')) { - layers { + layered { includeLayerTools = project.hasProperty('excludeTools') && project.getProperty('excludeTools') ? false : true } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLibraryCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLibraryCoordinates.java new file mode 100644 index 0000000000..d0eb6f06e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/DefaultLibraryCoordinates.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java index f6a588eb55..6664073164 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarModeLibrary.java @@ -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(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java index 8b813f448d..484bc2286c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java @@ -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-]+$"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java index f6b438a5bb..c998bb6088 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java @@ -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 { /** * 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 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 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java index 761703e706..5b77a3b17d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java @@ -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(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java index e7b0712f05..2a8f9ea82e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java @@ -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 stream() { + return LAYERS.stream(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/LocationFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java similarity index 60% rename from spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/LocationFilter.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java index d76b3d8188..734ea7e260 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/LocationFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java @@ -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 { private static final AntPathMatcher MATCHER = new AntPathMatcher(); - public LocationFilter(List includes, List 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 toMatch) { - return toMatch.stream().anyMatch((pattern) -> MATCHER.match(pattern, resourceName)); + public boolean matches(String path) { + return MATCHER.match(this.pattern, path); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java similarity index 59% rename from spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java index 0a164fc042..6818348447 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java @@ -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 the content type + * @since 2.3.0 */ -package org.springframework.boot.loader.tools.layer.application; +@FunctionalInterface +public interface ContentFilter { + + /** + * Return if the filter matches the specified item. + * @param item the item to test + * @return if the filter matches + */ + boolean matches(T item); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java similarity index 57% rename from spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java index 1d23d16032..3f63bf79bb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java @@ -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 the content type * @author Madhura Bhave + * @author Phillip Webb * @since 2.3.0 + * @see IncludeExcludeContentSelector */ -public interface LibraryStrategy extends Serializable { +public interface ContentSelector { /** - * 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} + * Return the {@link Layer} that the selector represents. + * @return the named layer */ - Layer getMatchingLayer(Library library); + Layer getLayer(); + + /** + * Returns {@code true} if the specified item is contained in this selection. + * @param item the item to test + * @return if the item is contained + */ + boolean contains(T item); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java index a365a4370c..0d406d924e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java @@ -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 layers; - private final List resourceStrategies; + private final List> applicationSelectors; - private final List libraryStrategies; + private final List> librarySelectors; - public CustomLayers(List layers, List resourceStrategies, - List libraryStrategies) { + public CustomLayers(List layers, List> applicationSelectors, + List> 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 void validateSelectorLayers(List> selectors, List layers) { + for (ContentSelector selector : selectors) { + validateSelectorLayers(selector, layers); + } + } + + private static void validateSelectorLayers(ContentSelector selector, List 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 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 Layer selectLayer(T item, List> selectors, Supplier name) { + for (ContentSelector selector : selectors) { + if (selector.contains(item)) { + return selector.getLayer(); + } } + throw new IllegalStateException(name.get() + " did not match any layer"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java new file mode 100644 index 0000000000..194a57a3e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java @@ -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 the content type + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public class IncludeExcludeContentSelector implements ContentSelector { + + private final Layer layer; + + private final List> includes; + + private final List> excludes; + + public IncludeExcludeContentSelector(Layer layer, List> includes, + List> excludes) { + this(layer, includes, excludes, Function.identity()); + } + + public IncludeExcludeContentSelector(Layer layer, List includes, List excludes, + Function> 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 List> adapt(List list, Function> 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 include : this.includes) { + if (include.matches(item)) { + return true; + } + } + return false; + } + + private boolean isExcluded(T item) { + if (this.excludes.isEmpty()) { + return false; + } + for (ContentFilter exclude : this.excludes) { + if (exclude.matches(item)) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java new file mode 100644 index 0000000000..295994c6bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java @@ -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 { + + 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(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/AbstractResourceFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/AbstractResourceFilter.java deleted file mode 100644 index 0f9040ee89..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/AbstractResourceFilter.java +++ /dev/null @@ -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 includes = new ArrayList<>(); - - private final List excludes = new ArrayList<>(); - - public AbstractResourceFilter(List includes, List 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 toMatch); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategy.java deleted file mode 100644 index f63d16491b..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategy.java +++ /dev/null @@ -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 filters = new ArrayList<>(); - - public FilteredResourceStrategy(String layer, List 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; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceFilter.java deleted file mode 100644 index 2047af2cc3..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceFilter.java +++ /dev/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); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceStrategy.java deleted file mode 100644 index ef4d97662a..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/application/ResourceStrategy.java +++ /dev/null @@ -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); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java deleted file mode 100644 index 06137e815e..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java +++ /dev/null @@ -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 includes; - - private final List excludes; - - public CoordinateFilter(List includes, List 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 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; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java deleted file mode 100644 index 3cb6cfb015..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java +++ /dev/null @@ -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 filters = new ArrayList<>(); - - public FilteredLibraryStrategy(String layer, List 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; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java deleted file mode 100644 index 7b21995f5b..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java +++ /dev/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); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/package-info.java deleted file mode 100644 index e58eb5b15d..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/package-info.java +++ /dev/null @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index edd016369c..dfeeec57ff 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -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

{ return this.layers.iterator(); } + @Override + public Stream stream() { + return this.layers.stream(); + } + @Override public Layer getLayer(String name) { return DEFAULT_LAYER; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java index 1971662ed2..daebaecbfd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java @@ -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"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java new file mode 100644 index 0000000000..36bbec3084 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java deleted file mode 100644 index 1393fe0914..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java +++ /dev/null @@ -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 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; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java new file mode 100644 index 0000000000..f5efbfe924 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java @@ -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 includes = Arrays.asList(); + List excludes = Arrays.asList(); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenNullIncludesAndEmptyExcludesReturnsTrue() { + List includes = null; + List excludes = null; + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenEmptyIncludesAndNotExcludedReturnsTrue() { + List includes = Arrays.asList(); + List excludes = Arrays.asList("B"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenEmptyIncludesAndExcludedReturnsFalse() { + List includes = Arrays.asList(); + List excludes = Arrays.asList("A"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isFalse(); + } + + @Test + void containsWhenIncludedAndEmptyExcludesReturnsTrue() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList(); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("B")).isTrue(); + } + + @Test + void containsWhenIncludedAndNotExcludedReturnsTrue() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList("C", "D"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("B")).isTrue(); + } + + @Test + void containsWhenIncludedAndExcludedReturnsFalse() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList("C", "D"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("C")).isFalse(); + } + + /** + * {@link ContentFilter} used for testing. + */ + static class TestContentsFilter implements ContentFilter { + + private final String match; + + TestContentsFilter(String match) { + this.match = match; + } + + @Override + public boolean matches(String item) { + return this.match.equals(item); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java new file mode 100644 index 0000000000..bcdaf986d2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategyTests.java deleted file mode 100644 index 7b91c6aa3b..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/FilteredResourceStrategyTests.java +++ /dev/null @@ -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 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"); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/LocationFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/LocationFilterTests.java deleted file mode 100644 index 01f4d01cda..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/application/LocationFilterTests.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java deleted file mode 100644 index 864f27add7..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java +++ /dev/null @@ -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 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 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 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 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 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 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 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 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(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java deleted file mode 100644 index 5607512d2f..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java +++ /dev/null @@ -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 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); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 8e8bf1f344..c522ac454a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -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 jarHash = new AtomicReference<>(); mavenBuild.project("jar-output-timestamp").execute((project) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml index 0759337048..71581fb99f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -1,35 +1,23 @@ - - + + + + **/application*.* + + + + + + *:*:*-SNAPSHOT + + + + configuration application snapshot-dependencies my-dependencies-name - - - - - *:*:*-SNAPSHOT - - - - - *:*:* - - - - - - - **/application*.* - - - - - ** - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index e3e3225b3a..8acb33c91a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java index 444aedb350..ecb7bbfe16 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java @@ -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(); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java index 4832e9eb6b..ffdcb43f8e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java @@ -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 layers = new ArrayList<>(); - List libraryStrategies = new ArrayList<>(); - List 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); + List> applicationSelectors = getApplicationSelectors(root); + List> librarySelectors = getLibrarySelectors(root); + List layers = getLayers(root); + return new CustomLayers(layers, applicationSelectors, librarySelectors); + } + + private List> getApplicationSelectors(Element root) { + return getSelectors(root, "application", ApplicationContentFilter::new); + } + + private List> getLibrarySelectors(Element root) { + return getSelectors(root, "dependencies", LibraryContentFilter::new); + } + + private List getLayers(Element root) { + Element layerOrder = getChildElement(root, "layerOrder"); + if (layerOrder == null) { + return Collections.emptyList(); + } + return getChildNodeTextContent(layerOrder, "layer").stream().map(Layer::new).collect(Collectors.toList()); + } + + private List> getSelectors(Element root, String elementName, + Function> filterFactory) { + Element element = getChildElement(root, elementName); + if (element == null) { + return Collections.emptyList(); + } + List> 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 selector = getSelector((Element) child, filterFactory); + selectors.add(selector); } } - return new CustomLayers(layers, resourceStrategies, libraryStrategies); + return selectors; } - private void processNode(List layers, List libraryStrategies, - List 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) FilteredLibraryStrategy::new, - CoordinateFilter::new, "coordinates"::equals)); - } - if ("application".equals(nodeName)) { - resourceStrategies.addAll(getStrategies(contents, - (StrategyFactory) FilteredResourceStrategy::new, - LocationFilter::new, "locations"::equals)); - } + private ContentSelector getSelector(Element element, Function> filterFactory) { + Layer layer = new Layer(element.getAttribute("layer")); + List includes = getChildNodeTextContent(element, "include"); + List excludes = getChildNodeTextContent(element, "exclude"); + return new IncludeExcludeContentSelector<>(layer, includes, excludes, filterFactory); } - private List getLayers(Element element) { - List 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 getStrategies(NodeList nodes, StrategyFactory strategyFactory, - FilterFactory filterFactory, Predicate filterPredicate) { - List 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 filters = getFilters(node, filterFactory, filterPredicate); - String layer = element.getAttribute("layer"); - contents.add(strategyFactory.getStrategy(layer, filters)); - } - } - } - return contents; - } - - private List getFilters(Node node, FilterFactory factory, Predicate predicate) { - NodeList childNodes = node.getChildNodes(); - Assert.state(childNodes.getLength() > 0, "Filters for layer-content must not be empty."); - List filters = new ArrayList<>(); - for (int i = 0; i < childNodes.getLength(); i++) { - Node childNode = childNodes.item(i); - if (childNode instanceof Element) { - List include = getPatterns((Element) childNode, "include"); - List exclude = getPatterns((Element) childNode, "exclude"); - if (predicate.test(childNode.getNodeName())) { - filters.add(factory.getFilter(include, exclude)); - } - } - } - return filters; - } - - private List getPatterns(Element element, String key) { + private List getChildNodeTextContent(Element element, String tagName) { List 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 { - - T getStrategy(String layer, List filters); - - } - - interface FilterFactory { - - E getFilter(List includes, List 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); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd new file mode 100644 index 0000000000..01eca01439 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration-2.3.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration-2.3.xsd deleted file mode 100644 index 95c8c352c1..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration-2.3.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java index 43f4dfb9e2..a4aac074ca 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml new file mode 100644 index 0000000000..7dafca3141 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + + + + my-layer + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml new file mode 100644 index 0000000000..fb45f1d7c2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + + + + my-deps + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml index 3e1bf3df49..64a7e9eb18 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,47 +1,32 @@ - - - configuration - application - my-resources - snapshot-dependencies + + + + META-INF/resources/** + *.properties + + + **/application*.* + + + + + + *:*:*-SNAPSHOT + + + com.acme:* + + + + my-deps my-dependencies-name - - - - - *:*:*-SNAPSHOT - - - - - com.acme:* - - - - - *:*:* - - - - - - - META-INF/resources/** - - - - - **/application*.* - - - - - ** - - - - \ No newline at end of file + snapshot-dependencies + my-resources + configuration + application + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml deleted file mode 100644 index 8dabc5eb88..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - my-deps - - - - - \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index 700eb37af4..089ba63ff1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,11 +1,11 @@ - - - my-layer - + - + - \ No newline at end of file + + my-layer + + \ No newline at end of file