From 65672a115080346e837ee2ad5974ce04c826f37d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 6 Apr 2020 15:40:54 -0700 Subject: [PATCH] Use a more compact layers.idx format Update the `layers.idx` format so that it is more compact and can be parsed by third-parties as YAML. Closes gh-20860 --- .../bundling/BootJarIntegrationTests.java | 75 ++++++--- .../gradle/tasks/bundling/BootJarTests.java | 65 +++++--- .../jarmode/layertools/IndexedLayers.java | 33 ++-- .../jarmode/layertools/HelpCommandTests.java | 11 +- .../layertools/IndexedLayersTests.java | 7 + .../layertools/LayerToolsJarModeTests.java | 11 +- .../jarmode/layertools/ListCommandTests.java | 11 +- .../boot/jarmode/layertools/test-layers.idx | 9 +- .../boot/loader/tools/LayersIndex.java | 94 ++++++++++-- .../loader/tools/AbstractPackagerTests.java | 101 +++---------- .../boot/loader/tools/LayersIndexTests.java | 143 ++++++++++++++++++ .../boot/loader/tools/LayersIndexTests-.txt | 6 + ...esInFolderAreInNotInSameLayerUsesFiles.txt | 8 + ...lFilesInFolderAreInSameLayerUsesFolder.txt | 4 + ...ests-writeToWhenLayerNotUsedSkipsLayer.txt | 6 + ...teToWhenSimpleNamesSortsAlphabetically.txt | 6 + ...sts-writeToWritesLayersInIteratorOrder.txt | 9 ++ .../boot/maven/JarIntegrationTests.java | 44 ++++-- 18 files changed, 475 insertions(+), 168 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInNotInSameLayerUsesFiles.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInSameLayerUsesFolder.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedSkipsLayer.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt 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 66923903fc..5f8b40e9d8 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 @@ -31,8 +31,9 @@ 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.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -103,30 +104,25 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - List expectedDependencies = new ArrayList<>(); + 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"); - List expectedSnapshotDependencies = new ArrayList<>(); + Set expectedSnapshotDependencies = new TreeSet<>(); expectedSnapshotDependencies.add("BOOT-INF/lib/commons-io-2.7-SNAPSHOT.jar"); (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); - assertThat(indexedLayers.get("spring-boot-loader")) - .allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate()); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); - assertThat(indexedLayers.get("application")).containsExactly("META-INF/MANIFEST.MF", - "BOOT-INF/classes/example/Main.class", "BOOT-INF/classes/static/file.txt", "BOOT-INF/classpath.idx", - "BOOT-INF/layers.idx"); + 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); - Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); - assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); - extractedLayers.forEach( - (name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name))); + assertExtractedLayers(layerNames, indexedLayers); } @TestTemplate @@ -151,7 +147,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"); assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); - List expectedDependencies = new ArrayList<>(); + 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<>(); @@ -160,23 +156,48 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { 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/file.txt"); + assertThat(indexedLayers.get("static")).containsExactly("BOOT-INF/classes/static/"); List appLayer = new ArrayList<>(indexedLayers.get("app")); - List nonLoaderEntries = Arrays.asList("META-INF/MANIFEST.MF", "BOOT-INF/classes/example/Main.class", - "BOOT-INF/classpath.idx", "BOOT-INF/layers.idx"); + 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).allMatch(Pattern.compile("org/springframework/boot/loader/.+\\.class").asPredicate()); + 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 assertExtractedLayers(List layerNames, Map> indexedLayers) + throws IOException { Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); - extractedLayers.forEach( - (name, contents) -> assertThat(contents).containsExactlyInAnyOrderElementsOf(indexedLayers.get(name))); + 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 writeMainClass() { @@ -213,11 +234,21 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { } 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)))) { - return reader.lines().map((line) -> line.split(" ")) - .collect(Collectors.groupingBy((layerAndPath) -> layerAndPath[0], LinkedHashMap::new, - Collectors.mapping((layerAndPath) -> layerAndPath[1], Collectors.toList()))); + 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; } } 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 48a16d7c19..3dfb492938 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -20,6 +20,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -103,12 +104,27 @@ class BootJarTests extends AbstractBootArchiveTests { List index = entryLines(jarFile, "BOOT-INF/layers.idx"); assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); - assertThat(index).contains("dependencies BOOT-INF/lib/first-library.jar", - "dependencies BOOT-INF/lib/second-library.jar", - "snapshot-dependencies BOOT-INF/lib/third-library-SNAPSHOT.jar", - "application BOOT-INF/classes/com/example/Application.class", - "application BOOT-INF/classes/application.properties", - "application BOOT-INF/classes/static/test.css"); + 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/second-library.jar\""); + if (!layerToolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + layerToolsJar + "\""); + } + expected.add("- \"spring-boot-loader\":"); + expected.add(" - \"org/\""); + expected.add("- \"snapshot-dependencies\":"); + 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); } } @@ -134,12 +150,25 @@ class BootJarTests extends AbstractBootArchiveTests { List index = entryLines(jarFile, "BOOT-INF/layers.idx"); assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); - assertThat(index).contains("my-internal-deps BOOT-INF/lib/first-library.jar", - "my-internal-deps BOOT-INF/lib/second-library.jar", - "my-snapshot-deps BOOT-INF/lib/third-library-SNAPSHOT.jar", - "application BOOT-INF/classes/com/example/Application.class", - "application BOOT-INF/classes/application.properties", - "resources BOOT-INF/classes/static/test.css"); + 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/second-library.jar\""); + expected.add("- \"my-snapshot-deps\":"); + 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); } } @@ -261,11 +290,13 @@ class BootJarTests extends AbstractBootArchiveTests { } private Set getLayerNames(List index) { - return index.stream().map(this::getLayerName).collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private String getLayerName(String indexLine) { - return indexLine.substring(0, indexLine.indexOf(" ")); + Set layerNames = new LinkedHashSet<>(); + for (String line : index) { + if (line.startsWith("- ")) { + layerNames.add(line.substring(3, line.length() - 2)); + } + } + return layerNames; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java index a76f43e0f0..d1e341249d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java @@ -31,6 +31,7 @@ import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file. @@ -43,12 +44,19 @@ class IndexedLayers implements Layers { private MultiValueMap layers = new LinkedMultiValueMap<>(); IndexedLayers(String indexFile) { - String[] lines = indexFile.split("\n"); - Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()).forEach((line) -> { - String[] content = line.split(" "); - Assert.state(content.length == 2, "Layer index file is malformed"); - this.layers.add(content[0], content[1]); - }); + String[] lines = Arrays.stream(indexFile.split("\n")).filter(StringUtils::hasText).toArray(String[]::new); + String layer = null; + for (String line : lines) { + if (line.startsWith("- ")) { + layer = line.substring(3, line.length() - 2); + } + else if (line.startsWith(" - ")) { + this.layers.add(layer, line.substring(5, line.length() - 1)); + } + else { + throw new IllegalStateException("Layer index file is malformed"); + } + } Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); } @@ -59,10 +67,15 @@ class IndexedLayers implements Layers { @Override public String getLayer(ZipEntry entry) { - String name = entry.getName(); - for (Map.Entry> indexEntry : this.layers.entrySet()) { - if (indexEntry.getValue().contains(name)) { - return indexEntry.getKey(); + return getLayer(entry.getName()); + } + + private String getLayer(String name) { + for (Map.Entry> entry : this.layers.entrySet()) { + for (String candidate : entry.getValue()) { + if (candidate.equals(name) || (candidate.endsWith("/") && name.startsWith(candidate))) { + return entry.getKey(); + } } } throw new IllegalStateException("No layer defined in index for file '" + name + "'"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java index 2ada64661c..3ef9de89c4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java @@ -77,10 +77,13 @@ class HelpCommandTests { JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); jarOutputStream.putNextEntry(indexEntry); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); - writer.write("0001 BOOT-INF/lib/a.jar\n"); - writer.write("0001 BOOT-INF/lib/b.jar\n"); - writer.write("0002 BOOT-INF/lib/c.jar\n"); - writer.write("0003 BOOT-INF/lib/d.jar\n"); + writer.write("- \"0001\":\n"); + writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); + writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); + writer.write("- \"0002\":\n"); + writer.write(" - \"0002 BOOT-INF/lib/c.jar\"\n"); + writer.write("- \"0003\":\n"); + writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); writer.flush(); } return file; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java index 4e5bb883ec..b4bc1e3a0b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java @@ -69,6 +69,13 @@ class IndexedLayersTests { .withMessage("No layer defined in index for file " + "'file.jar'"); } + @Test + void getLayerWhenMatchesFolderReturnsLayer() throws Exception { + IndexedLayers layers = new IndexedLayers(getIndex()); + assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); + assertThat(layers.getLayer(mockEntry("META-INF/a/sub/folder/and/a/file"))).isEqualTo("application"); + } + private String getIndex() throws Exception { ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass()); InputStreamReader reader = new InputStreamReader(resource.getInputStream()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java index a1ad8154ff..22ae825362 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java @@ -85,10 +85,13 @@ class LayerToolsJarModeTests { JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); jarOutputStream.putNextEntry(indexEntry); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); - writer.write("0001 BOOT-INF/lib/a.jar\n"); - writer.write("0001 BOOT-INF/lib/b.jar\n"); - writer.write("0002 BOOT-INF/lib/c.jar\n"); - writer.write("0003 BOOT-INF/lib/d.jar\n"); + writer.write("- \"0001\":\n"); + writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); + writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); + writer.write("- \"0002\":\n"); + writer.write(" - \"0002 BOOT-INF/lib/c.jar\"\n"); + writer.write("- \"0003\":\n"); + writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); writer.flush(); } return file; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java index 720f79eca2..4d2edc6ec1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java @@ -98,10 +98,13 @@ class ListCommandTests { JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); out.putNextEntry(indexEntry); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); - writer.write("0001 BOOT-INF/lib/a.jar\n"); - writer.write("0001 BOOT-INF/lib/b.jar\n"); - writer.write("0002 BOOT-INF/lib/c.jar\n"); - writer.write("0003 BOOT-INF/lib/d.jar\n"); + writer.write("- \"0001\":\n"); + writer.write(" - \"BOOT-INF/lib/a.jar\"\n"); + writer.write(" - \"BOOT-INF/lib/b.jar\"\n"); + writer.write("- \"0002\":\n"); + writer.write(" - \"0002 BOOT-INF/lib/c.jar\"\n"); + writer.write("- \"0003\":\n"); + writer.write(" - \"BOOT-INF/lib/d.jar\"\n"); writer.flush(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx index 2d5367a066..9825466349 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx @@ -1,3 +1,6 @@ -test BOOT-INF/lib/a.jar -test BOOT-INF/lib/b.jar -application BOOT-INF/classes/Demo.class +- "test": + - "BOOT-INF/lib/a.jar" + - "BOOT-INF/lib/b.jar" +- "application": + - "BOOT-INF/classes/Demo.class" + - "META-INF/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java index b12fce5b72..fc770770c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayersIndex.java @@ -21,23 +21,46 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * Index describing the layer to which each entry in a jar belongs. + * Index describing the layer to which each entry in a jar belongs. Index files are simple + * text files that should be read from top to bottom. Each file defines the layers and + * their content. Layers names are written as quoted strings prefixed by a dash space + * ({@code "- "}) and with a colon ({@code ":"}) suffix. Layer content is either a file or + * folder name written as a quoted string prefixed by space space dash space + * ({@code " - "}). A folder name ends with {@code /}, a file name does not. When a + * folder name is used it means that all files inside that folder are in the same layer. + *

+ * Index files are designed to be compatible with YAML and may be read into a list of + * `Map<String, List<String>>` instances. * * @author Madhura Bhave * @author Andy Wilkinson + * @author Phillip Webb * @since 2.3.0 */ public class LayersIndex { private final Iterable layers; - private final MultiValueMap index = new LinkedMultiValueMap<>(); + private final Node root = new Node(); + + /** + * Create a new {@link LayersIndex} backed by the given layers. + * @param layers the layers in the index + */ + public LayersIndex(Layer... layers) { + this(Arrays.asList(layers)); + } /** * Create a new {@link LayersIndex} backed by the given layers. @@ -53,7 +76,12 @@ public class LayersIndex { * @param name the name of the item */ public void add(Layer layer, String name) { - this.index.add(layer, name); + String[] segments = name.split("/"); + Node node = this.root; + for (int i = 0; i < segments.length; i++) { + boolean isFolder = i < (segments.length - 1); + node = node.updateOrAddNode(segments[i], isFolder, layer); + } } /** @@ -62,19 +90,67 @@ public class LayersIndex { * @throws IOException on IO error */ public void writeTo(OutputStream out) throws IOException { + MultiValueMap index = new LinkedMultiValueMap<>(); + this.root.buildIndex("", index); + index.values().forEach(Collections::sort); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); for (Layer layer : this.layers) { - List names = this.index.get(layer); - if (names != null) { + List names = index.get(layer); + if (names != null && !names.isEmpty()) { + writer.write("- \"" + layer + "\":\n"); for (String name : names) { - writer.write(layer.toString()); - writer.write(" "); - writer.write(name); - writer.write("\n"); + writer.write(" - \"" + name + "\"\n"); } } } writer.flush(); + } + + /** + * A node within the index represeting a single path segment. + */ + private static class Node { + + private final String name; + + private final Set layers; + + private final List children = new ArrayList<>(); + + Node() { + this.name = ""; + this.layers = new HashSet<>(); + } + + Node(String name, Layer layer, Node parent) { + this.name = name; + this.layers = new HashSet<>(Collections.singleton(layer)); + } + + Node updateOrAddNode(String segment, boolean isFolder, Layer layer) { + String name = segment + (isFolder ? "/" : ""); + for (Node child : this.children) { + if (name.equals(child.name)) { + child.layers.add(layer); + return child; + } + } + Node child = new Node(name, layer, this); + this.children.add(child); + return child; + } + + void buildIndex(String path, MultiValueMap index) { + String name = path + this.name; + if (this.layers.size() == 1) { + index.add(this.layers.iterator().next(), name); + } + else { + for (Node child : this.children) { + child.buildIndex(name, index); + } + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 6c39ef4777..7d3b575ae6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -268,84 +268,20 @@ abstract class AbstractPackagerTests

{ assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); List expectedLayers = new ArrayList<>(); - getExpectedLayers(expectedLayers); - expectedLayers.add("default " + "BOOT-INF/classpath.idx"); - expectedLayers.add("default " + "BOOT-INF/layers.idx"); - expectedLayers.add("0001 " + "BOOT-INF/lib/" + libJarFile1.getName()); - expectedLayers.add("0002 " + "BOOT-INF/lib/" + libJarFile2.getName()); - expectedLayers.add("0003 " + "BOOT-INF/lib/" + libJarFile3.getName()); - assertThat(Arrays.asList(layersIndex.split("\\n"))).containsExactly(expectedLayers.toArray(new String[0])); - } - - private void getExpectedLayers(List expectedLayers) { - expectedLayers.add("default META-INF/MANIFEST.MF"); - expectedLayers.add("default org/springframework/boot/loader/ClassPathIndexFile.class"); - expectedLayers.add("default org/springframework/boot/loader/ExecutableArchiveLauncher.class"); - expectedLayers.add("default org/springframework/boot/loader/JarLauncher.class"); - expectedLayers.add( - "default org/springframework/boot/loader/LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class"); - expectedLayers.add("default org/springframework/boot/loader/LaunchedURLClassLoader.class"); - expectedLayers.add("default org/springframework/boot/loader/Launcher.class"); - expectedLayers.add("default org/springframework/boot/loader/MainMethodRunner.class"); - expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher$1.class"); - expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class"); - expectedLayers - .add("default org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class"); - expectedLayers.add("default org/springframework/boot/loader/PropertiesLauncher.class"); - expectedLayers.add("default org/springframework/boot/loader/WarLauncher.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/Archive$Entry.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/Archive$EntryFilter.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/Archive.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$AbstractIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$ArchiveIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$EntryIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class"); - expectedLayers - .add("default org/springframework/boot/loader/archive/ExplodedArchive$SimpleJarFileArchive.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/ExplodedArchive.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$AbstractIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class"); - expectedLayers - .add("default org/springframework/boot/loader/archive/JarFileArchive$NestedArchiveIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/archive/JarFileArchive.class"); - expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessData.class"); - expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$1.class"); - expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class"); - expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile$FileAccess.class"); - expectedLayers.add("default org/springframework/boot/loader/data/RandomAccessDataFile.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/AsciiBytes.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/Bytes.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$1.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64End.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64Locator.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryParser.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/CentralDirectoryVisitor.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/FileHeader.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/Handler.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarEntry.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarEntryFilter.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$1.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$2.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFile$JarFileType.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFile.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries$1.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarFileEntries.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$1.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$2.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$CloseAction.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/JarURLConnection.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/StringSequence.class"); - expectedLayers.add("default org/springframework/boot/loader/jar/ZipInflaterInputStream.class"); - expectedLayers.add("default org/springframework/boot/loader/jarmode/JarMode.class"); - expectedLayers.add("default org/springframework/boot/loader/jarmode/JarModeLauncher.class"); - expectedLayers.add("default org/springframework/boot/loader/jarmode/TestJarMode.class"); - expectedLayers.add("default org/springframework/boot/loader/util/SystemPropertyUtils.class"); - expectedLayers.add("default BOOT-INF/classes/a/b/C.class"); + expectedLayers.add("- 'default':"); + expectedLayers.add(" - 'BOOT-INF/classes/'"); + expectedLayers.add(" - 'BOOT-INF/classpath.idx'"); + expectedLayers.add(" - 'BOOT-INF/layers.idx'"); + expectedLayers.add(" - 'META-INF/'"); + expectedLayers.add(" - 'org/'"); + expectedLayers.add("- '0001':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile1.getName() + "'"); + expectedLayers.add("- '0002':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile2.getName() + "'"); + expectedLayers.add("- '0003':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile3.getName() + "'"); + assertThat(layersIndex.split("\\n")) + .containsExactly(expectedLayers.stream().map((s) -> s.replace('\'', '"')).toArray(String[]::new)); } @Test @@ -360,8 +296,13 @@ abstract class AbstractPackagerTests

{ assertThat(Arrays.asList(classpathIndex.split("\\n"))).containsExactly("spring-boot-jarmode-layertools.jar"); assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); - assertThat(Arrays.stream(layersIndex.split("\\n")).map((n) -> n.split(" ")[0]).distinct()) - .containsExactly("default"); + List expectedLayers = new ArrayList<>(); + expectedLayers.add("- 'default':"); + expectedLayers.add(" - 'BOOT-INF/'"); + expectedLayers.add(" - 'META-INF/'"); + expectedLayers.add(" - 'org/'"); + assertThat(layersIndex.split("\\n")) + .containsExactly(expectedLayers.stream().map((s) -> s.replace('\'', '"')).toArray(String[]::new)); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java new file mode 100644 index 0000000000..27ff4bf86a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.AbstractObjectAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LayersIndex}. + * + * @author Phillip Webb + */ +class LayersIndexTests { + + private static final Layer LAYER_A = new Layer("a"); + + private static final Layer LAYER_B = new Layer("b"); + + private static final Layer LAYER_C = new Layer("c"); + + private String testMethodName; + + @BeforeEach + void setup(TestInfo testInfo) { + this.testMethodName = testInfo.getTestMethod().get().getName(); + } + + @Test + void writeToWhenSimpleNamesSortsAlphabetically() throws Exception { + LayersIndex index = new LayersIndex(LAYER_A); + index.add(LAYER_A, "cat"); + index.add(LAYER_A, "dog"); + index.add(LAYER_A, "aardvark"); + index.add(LAYER_A, "zerbra"); + index.add(LAYER_A, "hamster"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWritesLayersInIteratorOrder() { + LayersIndex index = new LayersIndex(LAYER_B, LAYER_A, LAYER_C); + index.add(LAYER_A, "a1"); + index.add(LAYER_A, "a2"); + index.add(LAYER_B, "b1"); + index.add(LAYER_B, "b2"); + index.add(LAYER_C, "c1"); + index.add(LAYER_C, "c2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenLayerNotUsedSkipsLayer() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1"); + index.add(LAYER_A, "a2"); + index.add(LAYER_C, "c1"); + index.add(LAYER_C, "c2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenAllFilesInFolderAreInSameLayerUsesFolder() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1/b1/c1"); + index.add(LAYER_A, "a1/b1/c2"); + index.add(LAYER_A, "a1/b2/c1"); + index.add(LAYER_B, "a2/b1"); + index.add(LAYER_B, "a2/b2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenAllFilesInFolderAreInNotInSameLayerUsesFiles() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1/b1/c1"); + index.add(LAYER_B, "a1/b1/c2"); + index.add(LAYER_C, "a1/b2/c1"); + index.add(LAYER_A, "a2/b1"); + index.add(LAYER_B, "a2/b2"); + assertThatIndex(index).writesExpectedContent(); + + } + + private LayersIndexAssert assertThatIndex(LayersIndex index) { + return new LayersIndexAssert(index); + } + + private class LayersIndexAssert extends AbstractObjectAssert { + + LayersIndexAssert(LayersIndex actual) { + super(actual, LayersIndexAssert.class); + } + + void writesExpectedContent() { + try { + String actualContent = getContent(); + String name = "LayersIndexTests-" + LayersIndexTests.this.testMethodName + ".txt"; + InputStream in = LayersIndexTests.class.getResourceAsStream(name); + Assert.state(in != null, "Can't read " + name); + String expectedContent = new String(FileCopyUtils.copyToByteArray(in), StandardCharsets.UTF_8); + assertThat(actualContent).isEqualTo(expectedContent); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + + } + + private String getContent() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.actual.writeTo(out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt new file mode 100644 index 0000000000..ed75eeb15b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt @@ -0,0 +1,6 @@ +- "a" + - "aardvark" + - "cat" + - "dog" + - "hamster" + - "zerbra" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInNotInSameLayerUsesFiles.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInNotInSameLayerUsesFiles.txt new file mode 100644 index 0000000000..dedad372e8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInNotInSameLayerUsesFiles.txt @@ -0,0 +1,8 @@ +- "a": + - "a1/b1/c1" + - "a2/b1" +- "b": + - "a1/b1/c2" + - "a2/b2" +- "c": + - "a1/b2/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInSameLayerUsesFolder.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInSameLayerUsesFolder.txt new file mode 100644 index 0000000000..f18bd42656 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInFolderAreInSameLayerUsesFolder.txt @@ -0,0 +1,4 @@ +- "a": + - "a1/" +- "b": + - "a2/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedSkipsLayer.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedSkipsLayer.txt new file mode 100644 index 0000000000..4618afa671 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedSkipsLayer.txt @@ -0,0 +1,6 @@ +- "a": + - "a1" + - "a2" +- "c": + - "c1" + - "c2" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt new file mode 100644 index 0000000000..420b66baa7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt @@ -0,0 +1,6 @@ +- "a": + - "aardvark" + - "cat" + - "dog" + - "hamster" + - "zerbra" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt new file mode 100644 index 0000000000..d7d29b1076 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt @@ -0,0 +1,9 @@ +- "b": + - "b1" + - "b2" +- "a": + - "a1" + - "a2" +- "c": + - "c1" + - "c2" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 94d6d707b3..2cec3d0b7e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -15,12 +15,14 @@ */ package org.springframework.boot.maven; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.JarFile; import java.util.stream.Collectors; @@ -31,7 +33,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.JarModeLibrary; -import org.springframework.util.FileCopyUtils; import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -305,12 +306,9 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); try { try (JarFile jarFile = new JarFile(repackaged)) { - ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx"); - InputStream inputStream = jarFile.getInputStream(entry); - InputStreamReader reader = new InputStreamReader(inputStream); - String[] lines = FileCopyUtils.copyToString(reader).split("\\n"); - assertThat(Arrays.stream(lines).map((n) -> n.split(" ")[0]).distinct()).containsExactly( - "dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", + "snapshot-dependencies", "application"); } } catch (IOException ex) { @@ -337,12 +335,9 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot"); try (JarFile jarFile = new JarFile(repackaged)) { - ZipEntry entry = jarFile.getEntry("BOOT-INF/layers.idx"); - InputStream inputStream = jarFile.getInputStream(entry); - InputStreamReader reader = new InputStreamReader(inputStream); - String[] lines = FileCopyUtils.copyToString(reader).split("\\n"); - assertThat(Arrays.stream(lines).map((n) -> n.split(" ")[0]).distinct()).containsExactly( - "my-dependencies-name", "snapshot-dependencies", "configuration", "application"); + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("my-dependencies-name", "snapshot-dependencies", + "configuration", "application"); } }); } @@ -378,4 +373,23 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { return jarHash.get(); } + 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; + } + } + }