From 1245e5eec9179338b9c7663d777ef201d8f065aa Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 1 Feb 2021 14:00:35 -0800 Subject: [PATCH] Add support for creating layered war files with Gradle See gh-22195 --- .../boot/gradle/tasks/bundling/BootWar.java | 52 ++- .../gradle/tasks/bundling/LayeredSpec.java | 20 +- .../AbstractBootArchiveIntegrationTests.java | 357 +++++++++++++++++- .../bundling/AbstractBootArchiveTests.java | 282 +++++++++++++- .../bundling/BootJarIntegrationTests.java | 354 +---------------- .../gradle/tasks/bundling/BootJarTests.java | 270 +------------ .../bundling/BootWarIntegrationTests.java | 15 +- .../gradle/tasks/bundling/BootWarTests.java | 20 +- ...tJarIntegrationTests-implicitLayers.gradle | 1 + ...ootWarIntegrationTests-customLayers.gradle | 50 +++ ...AreNotIncludedInTheArchiveByDefault.gradle | 6 + ...pendenciesCanBeIncludedInTheArchive.gradle | 6 + ...tWarIntegrationTests-implicitLayers.gradle | 33 ++ ...tionTests-jarTypeFilteringIsApplied.gradle | 6 + ...tionTests-layersWithCustomSourceSet.gradle | 36 ++ ...rationTests-multiModuleCustomLayers.gradle | 62 +++ ...tionTests-multiModuleImplicitLayers.gradle | 40 ++ ...hLayerToolsAndThenWithoutLayerTools.gradle | 12 + ...BuiltWithoutLayersAndThenWithLayers.gradle | 12 + ...ltLayeredAndThenWithExplicitLayered.gradle | 10 + 20 files changed, 1019 insertions(+), 625 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle 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 ddeac3b6f1..78ee717398 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 @@ -22,6 +22,7 @@ import java.util.function.Function; import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; @@ -30,6 +31,8 @@ import org.gradle.api.internal.file.copy.CopyAction; import org.gradle.api.provider.Property; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.bundling.War; @@ -50,12 +53,18 @@ public class BootWar extends War implements BootArchive { private static final String LIB_DIRECTORY = "WEB-INF/lib/"; + private static final String LAYERS_INDEX = "WEB-INF/layers.idx"; + private final BootArchiveSupport support; private final Property mainClass; private FileCollection providedClasspath; + private final ResolvedDependencies resolvedDependencies = new ResolvedDependencies(); + + private LayeredSpec layered = new LayeredSpec(); + /** * Creates a new {@code BootWar} task. */ @@ -65,6 +74,14 @@ public class BootWar extends War implements BootArchive { getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles)); this.support.moveModuleInfoToRoot(getRootSpec()); getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles); + getProject().getConfigurations().all((configuration) -> { + ResolvableDependencies incoming = configuration.getIncoming(); + incoming.afterResolve((resolvableDependencies) -> { + if (resolvableDependencies == incoming) { + this.resolvedDependencies.processConfiguration(configuration); + } + }); + }); } private Object getProvidedLibFiles() { @@ -74,12 +91,21 @@ public class BootWar extends War implements BootArchive { @Override public void copy() { this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, null, - null); + (isLayeredDisabled()) ? null : LAYERS_INDEX); super.copy(); } + private boolean isLayeredDisabled() { + return this.layered != null && !this.layered.isEnabled(); + } + @Override protected CopyAction createCopyAction() { + if (!isLayeredDisabled()) { + LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); + String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null; + return this.support.createCopyAction(this, layerResolver, layerToolsLocation); + } return this.support.createCopyAction(this); } @@ -181,6 +207,25 @@ public class BootWar extends War implements BootArchive { return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED; } + /** + * Returns the spec that describes the layers in a layered jar. + * @return the spec for the layers + * @since 2.5.0 + */ + @Nested + public LayeredSpec getLayered() { + return this.layered; + } + + /** + * Configures the war's layering using the given {@code action}. + * @param action the action to apply + * @since 2.5.0 + */ + public void layered(Action action) { + action.execute(this.layered); + } + /** * 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. @@ -201,6 +246,11 @@ public class BootWar extends War implements BootArchive { return launchScript; } + @Internal + ResolvedDependencies getResolvedDependencies() { + return this.resolvedDependencies; + } + /** * Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read. * @param the result type 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 index 01ed17d9ef..bad5b700e5 100644 --- 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 @@ -41,7 +41,7 @@ import org.springframework.boot.loader.tools.layer.LibraryContentFilter; import org.springframework.util.Assert; /** - * Encapsulates the configuration for a layered jar. + * Encapsulates the configuration for a layered archive. * * @author Madhura Bhave * @author Scott Frederick @@ -65,7 +65,7 @@ public class LayeredSpec { /** * Returns whether the layer tools should be included as a dependency in the layered - * jar. + * archive. * @return whether the layer tools should be included */ @Input @@ -74,7 +74,8 @@ public class LayeredSpec { } /** - * Sets whether the layer tools should be included as a dependency in the layered jar. + * Sets whether the layer tools should be included as a dependency in the layered + * archive. * @param includeLayerTools {@code true} if the layer tools should be included, * otherwise {@code false} */ @@ -83,7 +84,7 @@ public class LayeredSpec { } /** - * Returns whether the layers.idx should be included in the jar. + * Returns whether the layers.idx should be included in the archive. * @return whether the layers.idx should be included */ @Input @@ -92,8 +93,8 @@ public class LayeredSpec { } /** - * Sets whether the layers.idx should be included in the jar. - * @param enabled {@code true} layers.idx should be included in the jar, otherwise + * Sets whether the layers.idx should be included in the archive. + * @param enabled {@code true} layers.idx should be included in the archive, otherwise * {@code false} */ public void setEnabled(boolean enabled) { @@ -171,7 +172,8 @@ public class LayeredSpec { } /** - * Returns the order of the layers in the jar from least to most frequently changing. + * Returns the order of the layers in the archive from least to most frequently + * changing. * @return the layer order */ @Input @@ -180,7 +182,7 @@ public class LayeredSpec { } /** - * Sets to order of the layers in the jar from least to most frequently changing. + * Sets the order of the layers in the archive from least to most frequently changing. * @param layerOrder the layer order */ public void setLayerOrder(String... layerOrder) { @@ -188,7 +190,7 @@ public class LayeredSpec { } /** - * Sets to order of the layers in the jar from least to most frequently changing. + * Sets the order of the layers in the archive from least to most frequently changing. * @param layerOrder the layer order */ public void setLayerOrder(List layerOrder) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index 37ff902e93..eddc64cd1a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -16,18 +16,36 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.InvalidRunnerConfigurationException; import org.gradle.testkit.runner.TaskOutcome; import org.gradle.testkit.runner.UnexpectedBuildFailure; @@ -35,14 +53,17 @@ import org.junit.jupiter.api.TestTemplate; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.util.FileSystemUtils; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link BootJar}. + * Integration tests for {@link BootJar} and {@link BootWar}. * * @author Andy Wilkinson + * @author Madhura Bhave */ abstract class AbstractBootArchiveIntegrationTests { @@ -52,12 +73,16 @@ abstract class AbstractBootArchiveIntegrationTests { private final String classesPath; + private final String indexPath; + GradleBuild gradleBuild; - protected AbstractBootArchiveIntegrationTests(String taskName, String libPath, String classesPath) { + protected AbstractBootArchiveIntegrationTests(String taskName, String libPath, String classesPath, + String indexPath) { this.taskName = taskName; this.libPath = libPath; this.classesPath = classesPath; + this.indexPath = indexPath; } @TestTemplate @@ -204,6 +229,235 @@ abstract class AbstractBootArchiveIntegrationTests { .isEqualTo(TaskOutcome.UP_TO_DATE); } + @TestTemplate + void upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered() + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { + assertThat(this.gradleBuild.scriptProperty("layered", "").build("" + this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layered", "layered {}").build("" + this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @TestTemplate + void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers() + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { + assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = false").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = true").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools() + throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { + assertThat(this.gradleBuild.scriptProperty("layerTools", "").build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layerTools", "includeLayerTools = false").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void layersWithCustomSourceSet() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void implicitLayers() throws IOException { + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar"); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "jul-to-slf4j-1.7.28.jar"); + expectedDependencies.add(this.libPath + "log4j-api-2.12.1.jar"); + expectedDependencies.add(this.libPath + "log4j-to-slf4j-2.12.1.jar"); + expectedDependencies.add(this.libPath + "logback-classic-1.2.3.jar"); + expectedDependencies.add(this.libPath + "logback-core-1.2.3.jar"); + expectedDependencies.add(this.libPath + "slf4j-api-1.7.28.jar"); + expectedDependencies.add(this.libPath + "spring-boot-starter-logging-2.2.0.RELEASE.jar"); + Set expectedSnapshotDependencies = new TreeSet<>(); + expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")) + .containsExactly(getExpectedApplicationLayerContents(this.classesPath)); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + abstract String[] getExpectedApplicationLayerContents(String... additionalFiles); + + @TestTemplate + void multiModuleImplicitLayers() throws IOException { + writeSettingsGradle(); + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar"); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); + Set expectedSnapshotDependencies = new TreeSet<>(); + expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")).containsExactly(getExpectedApplicationLayerContents( + this.classesPath, this.libPath + "alpha-1.2.3.jar", this.libPath + "bravo-1.2.3.jar")); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + @TestTemplate + void customLayers() throws IOException { + writeMainClass(); + writeResource(); + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", + "static", "app"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); + List expectedSnapshotDependencies = new ArrayList<>(); + expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/"); + List appLayer = new ArrayList<>(indexedLayers.get("app")); + String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/"); + assertThat(appLayer).containsSubsequence(appLayerContents); + appLayer.removeAll(Arrays.asList(appLayerContents)); + assertThat(appLayer).containsExactly("org/"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + @TestTemplate + void multiModuleCustomLayers() throws IOException { + writeSettingsGradle(); + writeMainClass(); + writeResource(); + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", + "subproject-dependencies", "static", "app"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedSubprojectDependencies = new TreeSet<>(); + expectedSubprojectDependencies.add(this.libPath + "alpha-1.2.3.jar"); + expectedSubprojectDependencies.add(this.libPath + "bravo-1.2.3.jar"); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); + List expectedSnapshotDependencies = new ArrayList<>(); + expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); + (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + assertThat(indexedLayers.get("subproject-dependencies")) + .containsExactlyElementsOf(expectedSubprojectDependencies); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/"); + List appLayer = new ArrayList<>(indexedLayers.get("app")); + String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/"); + assertThat(appLayer).containsSubsequence(appLayerContents); + appLayer.removeAll(Arrays.asList(appLayerContents)); + assertThat(appLayer).containsExactly("org/"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + private void copyMainClassApplication() throws IOException { copyApplication("main"); } @@ -235,4 +489,103 @@ abstract class AbstractBootArchiveIntegrationTests { new JarOutputStream(new FileOutputStream(location), manifest).close(); } + private void writeSettingsGradle() { + try (PrintWriter writer = new PrintWriter( + new FileWriter(new File(this.gradleBuild.getProjectDir(), "settings.gradle")))) { + writer.println("include 'alpha', 'bravo'"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void writeMainClass() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) {"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void writeResource() { + try { + Path path = this.gradleBuild.getProjectDir().toPath() + .resolve(Paths.get("src", "main", "resources", "static", "file.txt")); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private Map> readLayerIndex(JarFile jarFile) throws IOException { + Map> index = new LinkedHashMap<>(); + ZipEntry indexEntry = jarFile.getEntry(this.indexPath + "layers.idx"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) { + String line = reader.readLine(); + String layer = null; + while (line != null) { + if (line.startsWith("- ")) { + layer = line.substring(3, line.length() - 2); + } + else if (line.startsWith(" - ")) { + index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1)); + } + line = reader.readLine(); + } + return index; + } + } + + private Map> readExtractedLayers(File root, List layerNames) throws IOException { + Map> extractedLayers = new LinkedHashMap<>(); + for (String layerName : layerNames) { + File layer = new File(root, layerName); + assertThat(layer).isDirectory(); + extractedLayers.put(layerName, + Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()).map(layer.toPath()::relativize) + .map(Path::toString).map(StringUtils::cleanPath).collect(Collectors.toList())); + } + return extractedLayers; + } + + private void assertExtractedLayers(List layerNames, Map> indexedLayers) + throws IOException { + Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); + assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); + extractedLayers.forEach((name, contents) -> { + List index = indexedLayers.get(name); + List unexpected = new ArrayList<>(); + for (String file : contents) { + if (!isInIndex(index, file)) { + unexpected.add(name); + } + } + assertThat(unexpected).isEmpty(); + }); + } + + private boolean isInIndex(List index, String file) { + for (String candidate : index) { + if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) { + return true; + } + } + return false; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 6582953e56..0ace9f6fcc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -16,10 +16,12 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermission; @@ -27,19 +29,35 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; +import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.artifacts.ResolvedModuleVersion; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.internal.file.archive.ZipCopyAction; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.gradle.api.tasks.bundling.Jar; @@ -49,8 +67,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.loader.tools.DefaultLaunchScript; +import org.springframework.boot.loader.tools.JarModeLibrary; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * Abstract base class for testing {@link BootArchive} implementations. @@ -72,15 +95,19 @@ abstract class AbstractBootArchiveTests { private final String classesPath; + private final String indexPath; + private Project project; private T task; - protected AbstractBootArchiveTests(Class taskClass, String launcherClass, String libPath, String classesPath) { + protected AbstractBootArchiveTests(Class taskClass, String launcherClass, String libPath, String classesPath, + String indexPath) { this.taskClass = taskClass; this.launcherClass = launcherClass; this.libPath = libPath; this.classesPath = classesPath; + this.indexPath = indexPath; } @BeforeEach @@ -407,6 +434,145 @@ abstract class AbstractBootArchiveTests { this.libPath + "third-library.jar"); } + @Test + void archiveShouldBeLayeredByDefault() throws IOException { + addContent(); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) + .isEqualTo(this.classesPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) + .isEqualTo(this.indexPath + "layers.idx"); + assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + } + } + + @Test + void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException { + List entryNames = getEntryNames(createLayeredJar((configuration) -> configuration.setEnabled(false))); + assertThat(entryNames).doesNotContain(this.indexPath + "layers.idx"); + } + + @Test + void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) + .isEqualTo(this.classesPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) + .isEqualTo(this.indexPath + "layers.idx"); + } + } + + @Test + void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + List entryNames = getEntryNames(jarFile); + assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar", + this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar", + this.libPath + "second-project-library-SNAPSHOT.jar", + this.classesPath + "com/example/Application.class", this.classesPath + "application.properties", + this.classesPath + "static/test.css"); + List index = entryLines(jarFile, this.indexPath + "layers.idx"); + assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", + "snapshot-dependencies", "application"); + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + List expected = new ArrayList<>(); + expected.add("- \"dependencies\":"); + expected.add(" - \"" + this.libPath + "first-library.jar\""); + expected.add(" - \"" + this.libPath + "first-project-library.jar\""); + expected.add(" - \"" + this.libPath + "second-library.jar\""); + if (!layerToolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + layerToolsJar + "\""); + } + expected.add("- \"spring-boot-loader\":"); + expected.add(" - \"org/\""); + expected.add("- \"snapshot-dependencies\":"); + expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\""); + if (layerToolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + layerToolsJar + "\""); + } + expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); + expected.add("- \"application\":"); + Set applicationContents = new TreeSet<>(); + applicationContents.add(" - \"" + this.classesPath + "\""); + if (archiveHasClasspathIndex()) { + applicationContents.add(" - \"" + this.indexPath + "classpath.idx\""); + } + applicationContents.add(" - \"" + this.indexPath + "layers.idx\""); + applicationContents.add(" - \"META-INF/\""); + expected.addAll(applicationContents); + assertThat(index).containsExactlyElementsOf(expected); + } + } + + @Test + void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() throws IOException { + 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.setLayerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); + }); + try (JarFile jarFile = new JarFile(jar)) { + List entryNames = getEntryNames(jar); + assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar", + this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar", + this.libPath + "second-project-library-SNAPSHOT.jar", + this.classesPath + "com/example/Application.class", this.classesPath + "application.properties", + this.classesPath + "static/test.css"); + List index = entryLines(jarFile, this.indexPath + "layers.idx"); + assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", + "resources", "application"); + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + List expected = new ArrayList<>(); + expected.add("- \"my-deps\":"); + expected.add(" - \"" + layerToolsJar + "\""); + expected.add("- \"my-internal-deps\":"); + expected.add(" - \"" + this.libPath + "first-library.jar\""); + expected.add(" - \"" + this.libPath + "first-project-library.jar\""); + expected.add(" - \"" + this.libPath + "second-library.jar\""); + expected.add("- \"my-snapshot-deps\":"); + expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\""); + expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); + expected.add("- \"resources\":"); + expected.add(" - \"" + this.classesPath + "static/\""); + expected.add("- \"application\":"); + Set applicationContents = new TreeSet<>(); + applicationContents.add(" - \"" + this.classesPath + "application.properties\""); + applicationContents.add(" - \"" + this.classesPath + "com/\""); + if (archiveHasClasspathIndex()) { + applicationContents.add(" - \"" + this.indexPath + "classpath.idx\""); + } + applicationContents.add(" - \"" + this.indexPath + "layers.idx\""); + applicationContents.add(" - \"META-INF/\""); + applicationContents.add(" - \"org/\""); + expected.addAll(applicationContents); + assertThat(index).containsExactlyElementsOf(expected); + } + } + + @Test + void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { + List entryNames = getEntryNames(createLayeredJar()); + assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + } + + @Test + void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { + List entryNames = getEntryNames( + createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false))); + assertThat(entryNames) + .doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); + } + protected File jarFile(String name) throws IOException { File file = newFile(name); try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) { @@ -453,4 +619,118 @@ abstract class AbstractBootArchiveTests { return file; } + File createLayeredJar() throws IOException { + return createLayeredJar((spec) -> { + }); + } + + File createLayeredJar(Action action) throws IOException { + applyLayered(action); + addContent(); + executeTask(); + return getTask().getArchiveFile().get().getAsFile(); + } + + abstract void applyLayered(Action action); + + boolean archiveHasClasspathIndex() { + return true; + } + + @SuppressWarnings("unchecked") + void addContent() throws IOException { + this.task.getMainClass().set("com.example.Main"); + File classesJavaMain = new File(this.temp, "classes/java/main"); + File applicationClass = new File(classesJavaMain, "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + File resourcesMain = new File(this.temp, "resources/main"); + File applicationProperties = new File(resourcesMain, "application.properties"); + applicationProperties.getParentFile().mkdirs(); + applicationProperties.createNewFile(); + File staticResources = new File(resourcesMain, "static"); + staticResources.mkdir(); + File css = new File(staticResources, "test.css"); + css.createNewFile(); + this.task.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), + jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"), + jarFile("second-project-library-SNAPSHOT.jar")); + 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")); + artifacts + .add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0")); + artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example", + "second-project-library", "1.0.0.SNAPSHOT")); + ResolvedConfiguration resolvedConfiguration = mock(ResolvedConfiguration.class); + given(resolvedConfiguration.getResolvedArtifacts()).willReturn(artifacts); + Configuration configuration = mock(Configuration.class); + given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration); + ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class); + given(configuration.getIncoming()).willReturn(resolvableDependencies); + DependencySet dependencies = mock(DependencySet.class); + DomainObjectSet projectDependencies = mock(DomainObjectSet.class); + given(dependencies.withType(ProjectDependency.class)).willReturn(projectDependencies); + given(configuration.getAllDependencies()).willReturn(dependencies); + willAnswer((invocation) -> { + invocation.getArgument(0, Action.class).execute(resolvableDependencies); + return null; + }).given(resolvableDependencies).afterResolve(any(Action.class)); + given(configuration.getIncoming()).willReturn(resolvableDependencies); + populateResolvedDependencies(configuration); + } + + abstract void populateResolvedDependencies(Configuration configuration); + + private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) { + ModuleComponentIdentifier moduleComponentIdentifier = mock(ModuleComponentIdentifier.class); + ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class); + given(libraryArtifactId.getComponentIdentifier()).willReturn(moduleComponentIdentifier); + ResolvedArtifact libraryArtifact = mockArtifact(fileName, group, module, version); + given(libraryArtifact.getId()).willReturn(libraryArtifactId); + return libraryArtifact; + } + + private ResolvedArtifact mockProjectArtifact(String fileName, String group, String module, String version) { + ProjectComponentIdentifier projectComponentIdentifier = mock(ProjectComponentIdentifier.class); + ComponentArtifactIdentifier projectArtifactId = mock(ComponentArtifactIdentifier.class); + given(projectArtifactId.getComponentIdentifier()).willReturn(projectComponentIdentifier); + ResolvedArtifact projectArtifact = mockArtifact(fileName, group, module, version); + given(projectArtifact.getId()).willReturn(projectArtifactId); + return projectArtifact; + } + + private ResolvedArtifact mockArtifact(String fileName, String group, String module, String version) { + ModuleVersionIdentifier moduleVersionIdentifier = mock(ModuleVersionIdentifier.class); + given(moduleVersionIdentifier.getGroup()).willReturn(group); + given(moduleVersionIdentifier.getName()).willReturn(module); + given(moduleVersionIdentifier.getVersion()).willReturn(version); + ResolvedModuleVersion moduleVersion = mock(ResolvedModuleVersion.class); + given(moduleVersion.getId()).willReturn(moduleVersionIdentifier); + ResolvedArtifact libraryArtifact = mock(ResolvedArtifact.class); + File file = new File(this.temp, fileName).getAbsoluteFile(); + given(libraryArtifact.getFile()).willReturn(file); + given(libraryArtifact.getModuleVersion()).willReturn(moduleVersion); + return libraryArtifact; + } + + List entryLines(JarFile jarFile, String entryName) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(entryName))))) { + return reader.lines().collect(Collectors.toList()); + } + } + + private Set getLayerNames(List index) { + Set layerNames = new LinkedHashSet<>(); + for (String line : index) { + if (line.startsWith("- ")) { + layerNames.add(line.substring(3, line.length() - 2)); + } + } + return layerNames; + } + } 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 70ce20fdc8..3400dd974c 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 @@ -16,36 +16,16 @@ package org.springframework.boot.gradle.tasks.bundling; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.StringReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.TreeSet; -import java.util.jar.JarFile; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; 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.gradle.junit.GradleCompatibility; -import org.springframework.boot.loader.tools.JarModeLibrary; -import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -60,233 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { BootJarIntegrationTests() { - super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/"); - } - - @TestTemplate - void upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { - assertThat(this.gradleBuild.scriptProperty("layered", "").build("bootJar").task(":bootJar").getOutcome()) - .isEqualTo(TaskOutcome.SUCCESS); - assertThat( - this.gradleBuild.scriptProperty("layered", "layered {}").build("bootJar").task(":bootJar").getOutcome()) - .isEqualTo(TaskOutcome.UP_TO_DATE); - } - - @TestTemplate - void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { - assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = false").build("bootJar") - .task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = true").build("bootJar") - .task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - } - - @TestTemplate - void notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure { - assertThat(this.gradleBuild.scriptProperty("layerTools", "").build("bootJar").task(":bootJar").getOutcome()) - .isEqualTo(TaskOutcome.SUCCESS); - assertThat(this.gradleBuild.scriptProperty("layerTools", "includeLayerTools = false").build("bootJar") - .task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - } - - @TestTemplate - void layersWithCustomSourceSet() throws IOException { - assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - } - - @TestTemplate - void implicitLayers() throws IOException { - writeMainClass(); - writeResource(); - assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - Map> indexedLayers; - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/library-1.0-SNAPSHOT.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); - indexedLayers = readLayerIndex(jarFile); - } - List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", - "application"); - assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - Set expectedDependencies = new TreeSet<>(); - expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); - Set expectedSnapshotDependencies = new TreeSet<>(); - expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); - assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); - assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); - assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); - assertThat(indexedLayers.get("application")).containsExactly("BOOT-INF/classes/", "BOOT-INF/classpath.idx", - "BOOT-INF/layers.idx", "META-INF/"); - BuildResult listLayers = this.gradleBuild.build("listLayers"); - assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - String listLayersOutput = listLayers.getOutput(); - assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); - BuildResult extractLayers = this.gradleBuild.build("extractLayers"); - assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertExtractedLayers(layerNames, indexedLayers); - } - - @TestTemplate - void multiModuleImplicitLayers() throws IOException { - writeSettingsGradle(); - writeMainClass(); - writeResource(); - assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - Map> indexedLayers; - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/alpha-1.2.3.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/bravo-1.2.3.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/library-1.0-SNAPSHOT.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); - indexedLayers = readLayerIndex(jarFile); - } - List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", - "application"); - assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - Set expectedDependencies = new TreeSet<>(); - expectedDependencies.add("BOOT-INF/lib/commons-lang3-3.9.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); - Set expectedSnapshotDependencies = new TreeSet<>(); - expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); - assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); - assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); - assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); - assertThat(indexedLayers.get("application")).containsExactly("BOOT-INF/classes/", "BOOT-INF/classpath.idx", - "BOOT-INF/layers.idx", "BOOT-INF/lib/alpha-1.2.3.jar", "BOOT-INF/lib/bravo-1.2.3.jar", "META-INF/"); - BuildResult listLayers = this.gradleBuild.build("listLayers"); - assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - String listLayersOutput = listLayers.getOutput(); - assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); - BuildResult extractLayers = this.gradleBuild.build("extractLayers"); - assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertExtractedLayers(layerNames, indexedLayers); - } - - @TestTemplate - void customLayers() throws IOException { - writeMainClass(); - writeResource(); - BuildResult build = this.gradleBuild.build("bootJar"); - assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - Map> indexedLayers; - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/library-1.0-SNAPSHOT.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull(); - indexedLayers = readLayerIndex(jarFile); - } - List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", - "static", "app"); - assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - Set expectedDependencies = new TreeSet<>(); - expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); - List expectedSnapshotDependencies = new ArrayList<>(); - expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); - assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); - assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar"); - assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); - assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/"); - List appLayer = new ArrayList<>(indexedLayers.get("app")); - Set nonLoaderEntries = new TreeSet<>(); - nonLoaderEntries.add("BOOT-INF/classes/example/"); - nonLoaderEntries.add("BOOT-INF/classpath.idx"); - nonLoaderEntries.add("BOOT-INF/layers.idx"); - nonLoaderEntries.add("META-INF/"); - assertThat(appLayer).containsSubsequence(nonLoaderEntries); - appLayer.removeAll(nonLoaderEntries); - assertThat(appLayer).containsExactly("org/"); - BuildResult listLayers = this.gradleBuild.build("listLayers"); - assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - String listLayersOutput = listLayers.getOutput(); - assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); - BuildResult extractLayers = this.gradleBuild.build("extractLayers"); - assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertExtractedLayers(layerNames, indexedLayers); - } - - @TestTemplate - void multiModuleCustomLayers() throws IOException { - writeSettingsGradle(); - writeMainClass(); - writeResource(); - BuildResult build = this.gradleBuild.build("bootJar"); - assertThat(build.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - Map> indexedLayers; - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/alpha-1.2.3.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/bravo-1.2.3.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/commons-lang3-3.9.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/lib/library-1.0-SNAPSHOT.jar")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/example/Main.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/static/file.txt")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/layers.idx")).isNotNull(); - indexedLayers = readLayerIndex(jarFile); - } - List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", - "subproject-dependencies", "static", "app"); - assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - Set expectedSubprojectDependencies = new TreeSet<>(); - expectedSubprojectDependencies.add("BOOT-INF/lib/alpha-1.2.3.jar"); - expectedSubprojectDependencies.add("BOOT-INF/lib/bravo-1.2.3.jar"); - Set expectedDependencies = new TreeSet<>(); - expectedDependencies.add("BOOT-INF/lib/spring-core-5.2.5.RELEASE.jar"); - expectedDependencies.add("BOOT-INF/lib/spring-jcl-5.2.5.RELEASE.jar"); - List expectedSnapshotDependencies = new ArrayList<>(); - expectedSnapshotDependencies.add("BOOT-INF/lib/library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); - assertThat(indexedLayers.get("subproject-dependencies")) - .containsExactlyElementsOf(expectedSubprojectDependencies); - assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); - assertThat(indexedLayers.get("commons-dependencies")).containsExactly("BOOT-INF/lib/commons-lang3-3.9.jar"); - assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); - assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/"); - List appLayer = new ArrayList<>(indexedLayers.get("app")); - Set nonLoaderEntries = new TreeSet<>(); - nonLoaderEntries.add("BOOT-INF/classes/example/"); - nonLoaderEntries.add("BOOT-INF/classpath.idx"); - nonLoaderEntries.add("BOOT-INF/layers.idx"); - nonLoaderEntries.add("META-INF/"); - assertThat(appLayer).containsSubsequence(nonLoaderEntries); - appLayer.removeAll(nonLoaderEntries); - assertThat(appLayer).containsExactly("org/"); - BuildResult listLayers = this.gradleBuild.build("listLayers"); - assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - String listLayersOutput = listLayers.getOutput(); - assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); - BuildResult extractLayers = this.gradleBuild.build("extractLayers"); - assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertExtractedLayers(layerNames, indexedLayers); + super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/"); } @TestTemplate @@ -320,107 +74,15 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { assertThat(output).doesNotContain("5. "); } - private void assertExtractedLayers(List layerNames, Map> indexedLayers) - throws IOException { - Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); - assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); - extractedLayers.forEach((name, contents) -> { - List index = indexedLayers.get(name); - List unexpected = new ArrayList<>(); - for (String file : contents) { - if (!isInIndex(index, file)) { - unexpected.add(name); - } - } - assertThat(unexpected).isEmpty(); - }); - } - - private boolean isInIndex(List index, String file) { - for (String candidate : index) { - if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) { - return true; - } - } - return false; - } - - private void writeSettingsGradle() { - try (PrintWriter writer = new PrintWriter( - new FileWriter(new File(this.gradleBuild.getProjectDir(), "settings.gradle")))) { - writer.println("include 'alpha', 'bravo'"); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private void writeMainClass() { - File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); - examplePackage.mkdirs(); - File main = new File(examplePackage, "Main.java"); - try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { - writer.println("package example;"); - writer.println(); - writer.println("import java.io.IOException;"); - writer.println(); - writer.println("public class Main {"); - writer.println(); - writer.println(" public static void main(String[] args) {"); - writer.println(" }"); - writer.println(); - writer.println("}"); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private void writeResource() { - try { - Path path = this.gradleBuild.getProjectDir().toPath() - .resolve(Paths.get("src", "main", "resources", "static", "file.txt")); - Files.createDirectories(path.getParent()); - Files.createFile(path); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private Map> readLayerIndex(JarFile jarFile) throws IOException { - Map> index = new LinkedHashMap<>(); - ZipEntry indexEntry = jarFile.getEntry("BOOT-INF/layers.idx"); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) { - String line = reader.readLine(); - String layer = null; - while (line != null) { - if (line.startsWith("- ")) { - layer = line.substring(3, line.length() - 2); - } - else if (line.startsWith(" - ")) { - index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1)); - } - line = reader.readLine(); - } - return index; - } - } - - private Map> readExtractedLayers(File root, List layerNames) throws IOException { - Map> extractedLayers = new LinkedHashMap<>(); - for (String layerName : layerNames) { - File layer = new File(root, layerName); - assertThat(layer).isDirectory(); - extractedLayers.put(layerName, - Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()).map(layer.toPath()::relativize) - .map(Path::toString).map(StringUtils::cleanPath).collect(Collectors.toList())); - } - return extractedLayers; - } - private void copyClasspathApplication() throws IOException { copyApplication("classpath"); } + @Override + String[] getExpectedApplicationLayerContents(String... additionalFiles) { + Set contents = new TreeSet<>(Arrays.asList(additionalFiles)); + contents.addAll(Arrays.asList("BOOT-INF/classpath.idx", "BOOT-INF/layers.idx", "META-INF/")); + return contents.toArray(new String[0]); + } + } 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 795c16e3e6..a8fa45c07c 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 @@ -16,41 +16,18 @@ package org.springframework.boot.gradle.tasks.bundling; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; import java.util.jar.JarFile; -import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.gradle.api.Action; -import org.gradle.api.DomainObjectSet; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.DependencySet; -import org.gradle.api.artifacts.ModuleVersionIdentifier; -import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.artifacts.ResolvableDependencies; -import org.gradle.api.artifacts.ResolvedArtifact; -import org.gradle.api.artifacts.ResolvedConfiguration; -import org.gradle.api.artifacts.ResolvedModuleVersion; -import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; -import org.gradle.api.artifacts.component.ModuleComponentIdentifier; -import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.junit.jupiter.api.Test; -import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; /** * Tests for {@link BootJar}. @@ -64,7 +41,8 @@ import static org.mockito.Mockito.mock; class BootJarTests extends AbstractBootArchiveTests { BootJarTests() { - super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/"); + super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/", + "BOOT-INF/"); } @Test @@ -89,130 +67,6 @@ class BootJarTests extends AbstractBootArchiveTests { } } - @Test - void jarShouldBeLayeredByDefault() throws IOException { - addContent(); - executeTask(); - BootJar bootJar = getTask(); - try (JarFile jarFile = new JarFile(bootJar.getArchiveFile().get().getAsFile())) { - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) - .isEqualTo("BOOT-INF/classes/"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")) - .isEqualTo("BOOT-INF/lib/"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index")) - .isEqualTo("BOOT-INF/classpath.idx"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) - .isEqualTo("BOOT-INF/layers.idx"); - assertThat(getEntryNames(jarFile)).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); - } - } - - @Test - void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException { - List entryNames = getEntryNames(createLayeredJar((configuration) -> configuration.setEnabled(false))); - assertThat(entryNames).doesNotContain("BOOT-INF/layers.idx"); - } - - @Test - void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException { - try (JarFile jarFile = new JarFile(createLayeredJar())) { - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) - .isEqualTo("BOOT-INF/classes/"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")) - .isEqualTo("BOOT-INF/lib/"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index")) - .isEqualTo("BOOT-INF/classpath.idx"); - assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) - .isEqualTo("BOOT-INF/layers.idx"); - } - } - - @Test - void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { - try (JarFile jarFile = new JarFile(createLayeredJar())) { - List entryNames = getEntryNames(jarFile); - assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", - "BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/lib/first-project-library.jar", - "BOOT-INF/lib/second-project-library-SNAPSHOT.jar", - "BOOT-INF/classes/com/example/Application.class", "BOOT-INF/classes/application.properties", - "BOOT-INF/classes/static/test.css"); - List index = entryLines(jarFile, "BOOT-INF/layers.idx"); - assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", - "snapshot-dependencies", "application"); - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - List expected = new ArrayList<>(); - expected.add("- \"dependencies\":"); - expected.add(" - \"BOOT-INF/lib/first-library.jar\""); - expected.add(" - \"BOOT-INF/lib/first-project-library.jar\""); - expected.add(" - \"BOOT-INF/lib/second-library.jar\""); - if (!layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); - } - expected.add("- \"spring-boot-loader\":"); - expected.add(" - \"org/\""); - expected.add("- \"snapshot-dependencies\":"); - expected.add(" - \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\""); - if (layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); - } - expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\""); - expected.add("- \"application\":"); - expected.add(" - \"BOOT-INF/classes/\""); - expected.add(" - \"BOOT-INF/classpath.idx\""); - expected.add(" - \"BOOT-INF/layers.idx\""); - expected.add(" - \"META-INF/\""); - assertThat(index).containsExactlyElementsOf(expected); - } - } - - @Test - void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() throws IOException { - 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.setLayerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); - }); - try (JarFile jarFile = new JarFile(jar)) { - List entryNames = getEntryNames(jar); - assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", - "BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/lib/first-project-library.jar", - "BOOT-INF/lib/second-project-library-SNAPSHOT.jar", - "BOOT-INF/classes/com/example/Application.class", "BOOT-INF/classes/application.properties", - "BOOT-INF/classes/static/test.css"); - List index = entryLines(jarFile, "BOOT-INF/layers.idx"); - assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", - "resources", "application"); - String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - List expected = new ArrayList<>(); - expected.add("- \"my-deps\":"); - expected.add(" - \"" + layerToolsJar + "\""); - expected.add("- \"my-internal-deps\":"); - expected.add(" - \"BOOT-INF/lib/first-library.jar\""); - expected.add(" - \"BOOT-INF/lib/first-project-library.jar\""); - expected.add(" - \"BOOT-INF/lib/second-library.jar\""); - expected.add("- \"my-snapshot-deps\":"); - expected.add(" - \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\""); - expected.add(" - \"BOOT-INF/lib/third-library-SNAPSHOT.jar\""); - expected.add("- \"resources\":"); - expected.add(" - \"BOOT-INF/classes/static/\""); - expected.add("- \"application\":"); - expected.add(" - \"BOOT-INF/classes/application.properties\""); - expected.add(" - \"BOOT-INF/classes/com/\""); - expected.add(" - \"BOOT-INF/classpath.idx\""); - expected.add(" - \"BOOT-INF/layers.idx\""); - expected.add(" - \"META-INF/\""); - expected.add(" - \"org/\""); - assertThat(index).containsExactlyElementsOf(expected); - } - } - @Test void jarsInLibAreStored() throws IOException { try (JarFile jarFile = new JarFile(createLayeredJar())) { @@ -237,19 +91,6 @@ class BootJarTests extends AbstractBootArchiveTests { } } - @Test - void whenJarIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { - List entryNames = getEntryNames(createLayeredJar()); - assertThat(entryNames).contains("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); - } - - @Test - void whenJarIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { - List entryNames = getEntryNames( - createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false))); - assertThat(entryNames).doesNotContain("BOOT-INF/layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); - } - @Test void classpathIndexPointsToBootInfLibs() throws IOException { try (JarFile jarFile = new JarFile(createPopulatedJar())) { @@ -268,111 +109,14 @@ class BootJarTests extends AbstractBootArchiveTests { return getTask().getArchiveFile().get().getAsFile(); } - private File createLayeredJar() throws IOException { - return createLayeredJar((spec) -> { - }); - } - - private File createLayeredJar(Action action) throws IOException { + @Override + void applyLayered(Action action) { getTask().layered(action); - addContent(); - executeTask(); - return getTask().getArchiveFile().get().getAsFile(); } - @SuppressWarnings("unchecked") - private void addContent() throws IOException { - BootJar bootJar = getTask(); - bootJar.getMainClass().set("com.example.Main"); - File classesJavaMain = new File(this.temp, "classes/java/main"); - File applicationClass = new File(classesJavaMain, "com/example/Application.class"); - applicationClass.getParentFile().mkdirs(); - applicationClass.createNewFile(); - File resourcesMain = new File(this.temp, "resources/main"); - File applicationProperties = new File(resourcesMain, "application.properties"); - applicationProperties.getParentFile().mkdirs(); - applicationProperties.createNewFile(); - File staticResources = new File(resourcesMain, "static"); - staticResources.mkdir(); - File css = new File(staticResources, "test.css"); - css.createNewFile(); - bootJar.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), - jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"), - jarFile("second-project-library-SNAPSHOT.jar")); - 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")); - artifacts - .add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0")); - artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example", - "second-project-library", "1.0.0.SNAPSHOT")); - ResolvedConfiguration resolvedConfiguration = mock(ResolvedConfiguration.class); - given(resolvedConfiguration.getResolvedArtifacts()).willReturn(artifacts); - Configuration configuration = mock(Configuration.class); - given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration); - ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class); - given(configuration.getIncoming()).willReturn(resolvableDependencies); - DependencySet dependencies = mock(DependencySet.class); - DomainObjectSet projectDependencies = mock(DomainObjectSet.class); - given(dependencies.withType(ProjectDependency.class)).willReturn(projectDependencies); - given(configuration.getAllDependencies()).willReturn(dependencies); - willAnswer((invocation) -> { - invocation.getArgument(0, Action.class).execute(resolvableDependencies); - return null; - }).given(resolvableDependencies).afterResolve(any(Action.class)); - given(configuration.getIncoming()).willReturn(resolvableDependencies); - bootJar.getResolvedDependencies().processConfiguration(configuration); - } - - private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) { - ModuleComponentIdentifier moduleComponentIdentifier = mock(ModuleComponentIdentifier.class); - ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class); - given(libraryArtifactId.getComponentIdentifier()).willReturn(moduleComponentIdentifier); - ResolvedArtifact libraryArtifact = mockArtifact(fileName, group, module, version); - given(libraryArtifact.getId()).willReturn(libraryArtifactId); - return libraryArtifact; - } - - private ResolvedArtifact mockProjectArtifact(String fileName, String group, String module, String version) { - ProjectComponentIdentifier projectComponentIdentifier = mock(ProjectComponentIdentifier.class); - ComponentArtifactIdentifier projectArtifactId = mock(ComponentArtifactIdentifier.class); - given(projectArtifactId.getComponentIdentifier()).willReturn(projectComponentIdentifier); - ResolvedArtifact projectArtifact = mockArtifact(fileName, group, module, version); - given(projectArtifact.getId()).willReturn(projectArtifactId); - return projectArtifact; - } - - private ResolvedArtifact mockArtifact(String fileName, String group, String module, String version) { - ModuleVersionIdentifier moduleVersionIdentifier = mock(ModuleVersionIdentifier.class); - given(moduleVersionIdentifier.getGroup()).willReturn(group); - given(moduleVersionIdentifier.getName()).willReturn(module); - given(moduleVersionIdentifier.getVersion()).willReturn(version); - ResolvedModuleVersion moduleVersion = mock(ResolvedModuleVersion.class); - given(moduleVersion.getId()).willReturn(moduleVersionIdentifier); - ResolvedArtifact libraryArtifact = mock(ResolvedArtifact.class); - File file = new File(this.temp, fileName).getAbsoluteFile(); - given(libraryArtifact.getFile()).willReturn(file); - given(libraryArtifact.getModuleVersion()).willReturn(moduleVersion); - return libraryArtifact; - } - - private List entryLines(JarFile jarFile, String entryName) throws IOException { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(entryName))))) { - return reader.lines().collect(Collectors.toList()); - } - } - - private Set getLayerNames(List index) { - Set layerNames = new LinkedHashSet<>(); - for (String line : index) { - if (line.startsWith("- ")) { - layerNames.add(line.substring(3, line.length() - 2)); - } - } - return layerNames; + @Override + void populateResolvedDependencies(Configuration configuration) { + getTask().getResolvedDependencies().processConfiguration(configuration); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java index 52ac318bcb..e7364adbd0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java @@ -16,10 +16,14 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + import org.springframework.boot.gradle.junit.GradleCompatibility; /** - * Integration tests for {@link BootJar}. + * Integration tests for {@link BootWar}. * * @author Andy Wilkinson */ @@ -27,7 +31,14 @@ import org.springframework.boot.gradle.junit.GradleCompatibility; class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests { BootWarIntegrationTests() { - super("bootWar", "WEB-INF/lib/", "WEB-INF/classes/"); + super("bootWar", "WEB-INF/lib/", "WEB-INF/classes/", "WEB-INF/"); + } + + @Override + String[] getExpectedApplicationLayerContents(String... additionalFiles) { + Set contents = new TreeSet<>(Arrays.asList(additionalFiles)); + contents.addAll(Arrays.asList("WEB-INF/layers.idx", "META-INF/")); + return contents.toArray(new String[0]); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java index 23cdeaec66..e162627129 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java @@ -20,6 +20,8 @@ import java.io.File; import java.io.IOException; import java.util.jar.JarFile; +import org.gradle.api.Action; +import org.gradle.api.artifacts.Configuration; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; class BootWarTests extends AbstractBootArchiveTests { BootWarTests() { - super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/"); + super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/", + "WEB-INF/"); } @Test @@ -111,4 +114,19 @@ class BootWarTests extends AbstractBootArchiveTests { getTask().copy(); } + @Override + void populateResolvedDependencies(Configuration configuration) { + getTask().getResolvedDependencies().processConfiguration(configuration); + } + + @Override + void applyLayered(Action action) { + getTask().layered(action); + } + + @Override + boolean archiveHasClasspathIndex() { + return false; + } + } 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 c9166bac23..cc3aa6f0e8 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 @@ -16,6 +16,7 @@ dependencies { implementation("com.example:library:1.0-SNAPSHOT") implementation("org.apache.commons:commons-lang3:3.9") implementation("org.springframework:spring-core:5.2.5.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE") } task listLayers(type: JavaExec) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle new file mode 100644 index 0000000000..0c4bcdcaf0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle @@ -0,0 +1,50 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + application { + 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"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle index 9354b000f6..85aea3ecce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -16,3 +16,9 @@ dependencies { developmentOnly("commons-io:commons-io:2.6") implementation("commons-io:commons-io:2.6") } + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle index 743a3a8e26..184c97603e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -19,3 +19,9 @@ dependencies { bootWar { classpath configurations.developmentOnly } + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle new file mode 100644 index 0000000000..6fd9018c45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle index 2839b33fcd..60e32af928 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -17,3 +17,9 @@ dependencies { implementation(name: "standard") implementation(name: "starter") } + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle new file mode 100644 index 0000000000..6892f814bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +sourceSets { + custom +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle new file mode 100644 index 0000000000..d9b51b6bc8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + application { + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" + } + intoLayer("app") + } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + excludeProjectDependencies() + } + intoLayer("subproject-dependencies") { + includeProjectDependencies() + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "subproject-dependencies", "static", "app"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle new file mode 100644 index 0000000000..f7ce127a7f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle new file mode 100644 index 0000000000..aa8d6fa822 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + {layerTools} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle new file mode 100644 index 0000000000..713e4c0dd0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + {layerEnablement} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle new file mode 100644 index 0000000000..4c45b1c35f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + {layered} +}