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;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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 org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
/**
* {@link Layers} implementation backed by a {@code BOOT-INF/layers.idx} file.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class IndexedLayers implements Layers {
private static final String APPLICATION_LAYER = "application";
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;
private MultiValueMap<String, String> layers = new LinkedMultiValueMap<>();
IndexedLayers(String indexFile) {
String[] lines = indexFile.split("\n");
this.layers = Arrays.stream(lines).map(String::trim).filter((line) -> !line.isEmpty())
.collect(Collectors.toCollection(ArrayList::new));
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]);
});
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
public Iterator<String> iterator() {
return this.layers.iterator();
return this.layers.keySet().iterator();
}
@Override
public String getLayer(ZipEntry entry) {
String name = entry.getName();
Matcher matcher = LAYER_PATTERN.matcher(name);
if (matcher.matches()) {
String layer = matcher.group(1);
Assert.state(this.layers.contains(layer), () -> "Unexpected layer '" + layer + "'");
return layer;
for (Map.Entry<String, List<String>> indexEntry : this.layers.entrySet()) {
if (indexEntry.getValue().contains(name)) {
return indexEntry.getKey();
}
}
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 {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n");
writer.write("b\n");
writer.write("c\n");
writer.write("d\n");
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.flush();
}
return file;
......
......@@ -16,10 +16,14 @@
package org.springframework.boot.jarmode.layertools;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry;
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.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
......@@ -29,6 +33,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link IndexedLayers}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class IndexedLayersTests {
......@@ -39,41 +44,35 @@ class IndexedLayersTests {
}
@Test
void createWhenIndexFileHasNoApplicationLayerAddSpringBootApplication() {
IndexedLayers layers = new IndexedLayers("test");
assertThat(layers).contains("springbootapplication");
void createWhenIndexFileIsMalformedThrowsException() throws Exception {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
.withMessage("Layer index file is malformed");
}
@Test
void iteratorReturnsLayers() {
IndexedLayers layers = new IndexedLayers("test\napplication");
void iteratorReturnsLayers() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers).containsExactly("test", "application");
}
@Test
void getLayerWhenMatchesLayerPatterReturnsLayer() {
IndexedLayers layers = new IndexedLayers("test");
assertThat(layers.getLayer(mockEntry("BOOT-INF/layers/test/lib/file.jar"))).isEqualTo("test");
void getLayerWhenMatchesNameReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
}
@Test
void getLayerWhenMatchesLayerPatterForMissingLayerThrowsException() {
IndexedLayers layers = new IndexedLayers("test");
assertThatIllegalStateException()
.isThrownBy(() -> layers.getLayer(mockEntry("BOOT-INF/layers/missing/lib/file.jar")))
.withMessage("Unexpected layer 'missing'");
void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
.withMessage("No layer defined in index for file " + "'file.jar'");
}
@Test
void getLayerWhenDoesNotMatchLayerPatternReturnsApplication() {
IndexedLayers layers = new IndexedLayers("test\napplication");
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application");
}
@Test
void getLayerWhenDoesNotMatchLayerPatternAndHasNoApplicationLayerReturnsSpringApplication() {
IndexedLayers layers = new IndexedLayers("test");
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("springbootapplication");
private String getIndex() throws Exception {
ClassPathResource resource = new ClassPathResource("test-layers.idx", getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
}
private ZipEntry mockEntry(String name) {
......
......@@ -85,10 +85,10 @@ class LayerToolsJarModeTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("a\n");
writer.write("b\n");
writer.write("c\n");
writer.write("d\n");
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.flush();
}
return file;
......
......@@ -39,6 +39,7 @@ import static org.mockito.BDDMockito.given;
* Tests for {@link ListCommand}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class ListCommandTests {
......@@ -74,7 +75,7 @@ class ListCommandTests {
File file = new File(this.temp, name);
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
writeLayersIndex(jarOutputStream);
String entryPrefix = "BOOT-INF/layers/";
String entryPrefix = "BOOT-INF/lib/";
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/"));
jarOutputStream.closeEntry();
jarOutputStream.putNextEntry(new ZipEntry(entryPrefix + "a/a.jar"));
......@@ -97,10 +98,10 @@ class ListCommandTests {
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
out.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
writer.write("a\n");
writer.write("b\n");
writer.write("c\n");
writer.write("d\n");
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.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