Skip to content

Commit d61a79d

Browse files
mbhavephilwebb
authored andcommitted
Support flat jar layering with layertools
Update layertools to support the flat jar format. Layers are now determined by reading the `layers.idx` file. Closes gh-20813
1 parent bfa04e6 commit d61a79d

File tree

7 files changed

+58
-64
lines changed

7 files changed

+58
-64
lines changed

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,59 +20,52 @@
2020
import java.io.IOException;
2121
import java.nio.charset.StandardCharsets;
2222
import java.nio.file.NoSuchFileException;
23-
import java.util.ArrayList;
2423
import java.util.Arrays;
2524
import java.util.Iterator;
2625
import java.util.List;
26+
import java.util.Map;
2727
import java.util.jar.JarFile;
28-
import java.util.regex.Matcher;
29-
import java.util.regex.Pattern;
30-
import java.util.stream.Collectors;
3128
import java.util.zip.ZipEntry;
3229

3330
import org.springframework.util.Assert;
31+
import org.springframework.util.LinkedMultiValueMap;
32+
import org.springframework.util.MultiValueMap;
3433
import org.springframework.util.StreamUtils;
3534

3635
/**
3736
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file.
3837
*
3938
* @author Phillip Webb
39+
* @author Madhura Bhave
4040
*/
4141
class IndexedLayers implements Layers {
4242

43-
private static final String APPLICATION_LAYER = "application";
44-
45-
private static final String SPRING_BOOT_APPLICATION_LAYER = "springbootapplication";
46-
47-
private static final Pattern LAYER_PATTERN = Pattern.compile("^BOOT-INF\\/layers\\/([a-zA-Z0-9-]+)\\/.*$");
48-
49-
private List<String> layers;
43+
private MultiValueMap<String, String> layers = new LinkedMultiValueMap<>();
5044

5145
IndexedLayers(String indexFile) {
5246
String[] lines = indexFile.split("\n");
53-
this.layers = Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty())
54-
.collect(Collectors.toCollection(ArrayList::new));
47+
Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()).forEach((line) -> {
48+
String[] content = line.split(" ");
49+
Assert.state(content.length == 2, "Layer index file is malformed");
50+
this.layers.add(content[0], content[1]);
51+
});
5552
Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded");
56-
if (!this.layers.contains(APPLICATION_LAYER)) {
57-
this.layers.add(0, SPRING_BOOT_APPLICATION_LAYER);
58-
}
5953
}
6054

6155
@Override
6256
public Iterator<String> iterator() {
63-
return this.layers.iterator();
57+
return this.layers.keySet().iterator();
6458
}
6559

6660
@Override
6761
public String getLayer(ZipEntry entry) {
6862
String name = entry.getName();
69-
Matcher matcher = LAYER_PATTERN.matcher(name);
70-
if (matcher.matches()) {
71-
String layer = matcher.group(1);
72-
Assert.state(this.layers.contains(layer), () -> "Unexpected layer '" + layer + "'");
73-
return layer;
63+
for (Map.Entry<String, List<String>> indexEntry : this.layers.entrySet()) {
64+
if (indexEntry.getValue().contains(name)) {
65+
return indexEntry.getKey();
66+
}
7467
}
75-
return this.layers.contains(APPLICATION_LAYER) ? APPLICATION_LAYER : SPRING_BOOT_APPLICATION_LAYER;
68+
throw new IllegalStateException("No layer defined in index for file '" + name + "'");
7669
}
7770

7871
/**

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ private File createJarFile(String name) throws IOException {
7777
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
7878
jarOutputStream.putNextEntry(indexEntry);
7979
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
80-
writer.write("a\n");
81-
writer.write("b\n");
82-
writer.write("c\n");
83-
writer.write("d\n");
80+
writer.write("0001 BOOT-INF/lib/a.jar\n");
81+
writer.write("0001 BOOT-INF/lib/b.jar\n");
82+
writer.write("0002 BOOT-INF/lib/c.jar\n");
83+
writer.write("0003 BOOT-INF/lib/d.jar\n");
8484
writer.flush();
8585
}
8686
return file;

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package org.springframework.boot.jarmode.layertools;
1818

19+
import java.io.InputStreamReader;
1920
import java.util.zip.ZipEntry;
2021

2122
import org.junit.jupiter.api.Test;
2223

24+
import org.springframework.core.io.ClassPathResource;
25+
import org.springframework.util.FileCopyUtils;
26+
2327
import static org.assertj.core.api.Assertions.assertThat;
2428
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
2529
import static org.mockito.BDDMockito.given;
@@ -29,6 +33,7 @@
2933
* Tests for {@link IndexedLayers}.
3034
*
3135
* @author Phillip Webb
36+
* @author Madhura Bhave
3237
*/
3338
class IndexedLayersTests {
3439

@@ -39,41 +44,35 @@ void createWhenIndexFileIsEmptyThrowsException() {
3944
}
4045

4146
@Test
42-
void createWhenIndexFileHasNoApplicationLayerAddSpringBootApplication() {
43-
IndexedLayers layers = new IndexedLayers("test");
44-
assertThat(layers).contains("springbootapplication");
47+
void createWhenIndexFileIsMalformedThrowsException() throws Exception {
48+
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
49+
.withMessage("Layer index file is malformed");
4550
}
4651

4752
@Test
48-
void iteratorReturnsLayers() {
49-
IndexedLayers layers = new IndexedLayers("test\napplication");
53+
void iteratorReturnsLayers() throws Exception {
54+
IndexedLayers layers = new IndexedLayers(getIndex());
5055
assertThat(layers).containsExactly("test", "application");
5156
}
5257

5358
@Test
54-
void getLayerWhenMatchesLayerPatterReturnsLayer() {
55-
IndexedLayers layers = new IndexedLayers("test");
56-
assertThat(layers.getLayer(mockEntry("BOOT-INF/layers/test/lib/file.jar"))).isEqualTo("test");
59+
void getLayerWhenMatchesNameReturnsLayer() throws Exception {
60+
IndexedLayers layers = new IndexedLayers(getIndex());
61+
assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
62+
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
5763
}
5864

5965
@Test
60-
void getLayerWhenMatchesLayerPatterForMissingLayerThrowsException() {
61-
IndexedLayers layers = new IndexedLayers("test");
62-
assertThatIllegalStateException()
63-
.isThrownBy(() -> layers.getLayer(mockEntry("BOOT-INF/layers/missing/lib/file.jar")))
64-
.withMessage("Unexpected layer 'missing'");
66+
void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
67+
IndexedLayers layers = new IndexedLayers(getIndex());
68+
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
69+
.withMessage("No layer defined in index for file " + "'file.jar'");
6570
}
6671

67-
@Test
68-
void getLayerWhenDoesNotMatchLayerPatternReturnsApplication() {
69-
IndexedLayers layers = new IndexedLayers("test\napplication");
70-
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application");
71-
}
72-
73-
@Test
74-
void getLayerWhenDoesNotMatchLayerPatternAndHasNoApplicationLayerReturnsSpringApplication() {
75-
IndexedLayers layers = new IndexedLayers("test");
76-
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("springbootapplication");
72+
private String getIndex() throws Exception {
73+
ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass());
74+
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
75+
return FileCopyUtils.copyToString(reader);
7776
}
7877

7978
private ZipEntry mockEntry(String name) {

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ private File createJarFile(String name) throws IOException {
8585
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
8686
jarOutputStream.putNextEntry(indexEntry);
8787
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
88-
writer.write("a\n");
89-
writer.write("b\n");
90-
writer.write("c\n");
91-
writer.write("d\n");
88+
writer.write("0001 BOOT-INF/lib/a.jar\n");
89+
writer.write("0001 BOOT-INF/lib/b.jar\n");
90+
writer.write("0002 BOOT-INF/lib/c.jar\n");
91+
writer.write("0003 BOOT-INF/lib/d.jar\n");
9292
writer.flush();
9393
}
9494
return file;

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* Tests for {@link ListCommand}.
4040
*
4141
* @author Phillip Webb
42+
* @author Madhura Bhave
4243
*/
4344
class ListCommandTests {
4445

@@ -74,7 +75,7 @@ private File createJarFile(String name) throws IOException {
7475
File file = new File(this.temp, name);
7576
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
7677
writeLayersIndex(jarOutputStream);
77-
String entryPrefix = "BOOT-INF/layers/";
78+
String entryPrefix = "BOOT-INF/lib/";
7879
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/"));
7980
jarOutputStream.closeEntry();
8081
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar"));
@@ -97,10 +98,10 @@ private void writeLayersIndex(ZipOutputStream out) throws IOException {
9798
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
9899
out.putNextEntry(indexEntry);
99100
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
100-
writer.write("a\n");
101-
writer.write("b\n");
102-
writer.write("c\n");
103-
writer.write("d\n");
101+
writer.write("0001 BOOT-INF/lib/a.jar\n");
102+
writer.write("0001 BOOT-INF/lib/b.jar\n");
103+
writer.write("0002 BOOT-INF/lib/c.jar\n");
104+
writer.write("0003 BOOT-INF/lib/d.jar\n");
104105
writer.flush();
105106
}
106107

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
springbootapplication
2-
a
3-
b
4-
c
5-
d
1+
0001
2+
0002
3+
0003
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test BOOT-INF/lib/a.jar
2+
test BOOT-INF/lib/b.jar
3+
application BOOT-INF/classes/Demo.class

0 commit comments

Comments
 (0)