Commit d61a79d9 authored by Madhura Bhave's avatar Madhura Bhave Committed by Phillip Webb

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
parent bfa04e65
...@@ -20,59 +20,52 @@ import java.io.FileNotFoundException; ...@@ -20,59 +20,52 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
/** /**
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file. * {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class IndexedLayers implements Layers { class IndexedLayers implements Layers {
private static final String APPLICATION_LAYER = "application"; private MultiValueMap<String, String> layers = new LinkedMultiValueMap<>();
private static final String SPRING_BOOT_APPLICATION_LAYER = "springbootapplication";
private static final Pattern LAYER_PATTERN = Pattern.compile("^BOOT-INF\\/layers\\/([a-zA-Z0-9-]+)\\/.*$");
private List<String> layers;
IndexedLayers(String indexFile) { IndexedLayers(String indexFile) {
String[] lines = indexFile.split("\n"); String[] lines = indexFile.split("\n");
this.layers = Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()) Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty()).forEach((line) -> {
.collect(Collectors.toCollection(ArrayList::new)); String[] content = line.split(" ");
Assert.state(content.length == 2, "Layer index file is malformed");
this.layers.add(content[0], content[1]);
});
Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded");
if (!this.layers.contains(APPLICATION_LAYER)) {
this.layers.add(0, SPRING_BOOT_APPLICATION_LAYER);
}
} }
@Override @Override
public Iterator<String> iterator() { public Iterator<String> iterator() {
return this.layers.iterator(); return this.layers.keySet().iterator();
} }
@Override @Override
public String getLayer(ZipEntry entry) { public String getLayer(ZipEntry entry) {
String name = entry.getName(); String name = entry.getName();
Matcher matcher = LAYER_PATTERN.matcher(name); for (Map.Entry<String, List<String>> indexEntry : this.layers.entrySet()) {
if (matcher.matches()) { if (indexEntry.getValue().contains(name)) {
String layer = matcher.group(1); return indexEntry.getKey();
Assert.state(this.layers.contains(layer), () -> "Unexpected layer '" + layer + "'"); }
return layer;
} }
return this.layers.contains(APPLICATION_LAYER) ? APPLICATION_LAYER : SPRING_BOOT_APPLICATION_LAYER; throw new IllegalStateException("No layer defined in index for file '" + name + "'");
} }
/** /**
......
...@@ -77,10 +77,10 @@ class HelpCommandTests { ...@@ -77,10 +77,10 @@ class HelpCommandTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry); jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }
return file; return file;
......
...@@ -16,10 +16,14 @@ ...@@ -16,10 +16,14 @@
package org.springframework.boot.jarmode.layertools; package org.springframework.boot.jarmode.layertools;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
...@@ -29,6 +33,7 @@ import static org.mockito.Mockito.mock; ...@@ -29,6 +33,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link IndexedLayers}. * Tests for {@link IndexedLayers}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class IndexedLayersTests { class IndexedLayersTests {
...@@ -39,41 +44,35 @@ class IndexedLayersTests { ...@@ -39,41 +44,35 @@ class IndexedLayersTests {
} }
@Test @Test
void createWhenIndexFileHasNoApplicationLayerAddSpringBootApplication() { void createWhenIndexFileIsMalformedThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
assertThat(layers).contains("springbootapplication"); .withMessage("Layer index file is malformed");
} }
@Test @Test
void iteratorReturnsLayers() { void iteratorReturnsLayers() throws Exception {
IndexedLayers layers = new IndexedLayers("test\napplication"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers).containsExactly("test", "application"); assertThat(layers).containsExactly("test", "application");
} }
@Test @Test
void getLayerWhenMatchesLayerPatterReturnsLayer() { void getLayerWhenMatchesNameReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers.getLayer(mockEntry("BOOT-INF/layers/test/lib/file.jar"))).isEqualTo("test"); assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
} }
@Test @Test
void getLayerWhenMatchesLayerPatterForMissingLayerThrowsException() { void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers("test"); IndexedLayers layers = new IndexedLayers(getIndex());
assertThatIllegalStateException() assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
.isThrownBy(() -> layers.getLayer(mockEntry("BOOT-INF/layers/missing/lib/file.jar"))) .withMessage("No layer defined in index for file " + "'file.jar'");
.withMessage("Unexpected layer 'missing'");
} }
@Test private String getIndex() throws Exception {
void getLayerWhenDoesNotMatchLayerPatternReturnsApplication() { ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass());
IndexedLayers layers = new IndexedLayers("test\napplication"); InputStreamReader reader = new InputStreamReader(resource.getInputStream());
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); return FileCopyUtils.copyToString(reader);
}
@Test
void getLayerWhenDoesNotMatchLayerPatternAndHasNoApplicationLayerReturnsSpringApplication() {
IndexedLayers layers = new IndexedLayers("test");
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("springbootapplication");
} }
private ZipEntry mockEntry(String name) { private ZipEntry mockEntry(String name) {
......
...@@ -85,10 +85,10 @@ class LayerToolsJarModeTests { ...@@ -85,10 +85,10 @@ class LayerToolsJarModeTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry); jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }
return file; return file;
......
...@@ -39,6 +39,7 @@ import static org.mockito.BDDMockito.given; ...@@ -39,6 +39,7 @@ import static org.mockito.BDDMockito.given;
* Tests for {@link ListCommand}. * Tests for {@link ListCommand}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Madhura Bhave
*/ */
class ListCommandTests { class ListCommandTests {
...@@ -74,7 +75,7 @@ class ListCommandTests { ...@@ -74,7 +75,7 @@ class ListCommandTests {
File file = new File(this.temp, name); File file = new File(this.temp, name);
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) { try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
writeLayersIndex(jarOutputStream); writeLayersIndex(jarOutputStream);
String entryPrefix = "BOOT-INF/layers/"; String entryPrefix = "BOOT-INF/lib/";
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/")); jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/"));
jarOutputStream.closeEntry(); jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar")); jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar"));
...@@ -97,10 +98,10 @@ class ListCommandTests { ...@@ -97,10 +98,10 @@ class ListCommandTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx"); JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
out.putNextEntry(indexEntry); out.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
writer.write("a\n"); writer.write("0001 BOOT-INF/lib/a.jar\n");
writer.write("b\n"); writer.write("0001 BOOT-INF/lib/b.jar\n");
writer.write("c\n"); writer.write("0002 BOOT-INF/lib/c.jar\n");
writer.write("d\n"); writer.write("0003 BOOT-INF/lib/d.jar\n");
writer.flush(); writer.flush();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment