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
This commit is contained in:
parent
bfa04e6574
commit
d61a79d90b
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
springbootapplication
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
0001
|
||||
0002
|
||||
0003
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
test BOOT-INF/lib/a.jar
|
||||
test BOOT-INF/lib/b.jar
|
||||
application BOOT-INF/classes/Demo.class
|
||||
Loading…
Reference in New Issue