diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java new file mode 100644 index 0000000000..a774a167b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ImplicitLayerResolver.java @@ -0,0 +1,58 @@ +/* + * 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; + +/** + * Implementation of {@link Layers} that uses implicit rules. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ImplicitLayerResolver extends StandardLayers { + + private static final String[] RESOURCE_LOCATIONS = { "META-INF/resources/", "resources/", "static/", "public/" }; + + @Override + public Layer getLayer(String name) { + if (!isClassFile(name) && isInResourceLocation(name)) { + return RESOURCES; + } + return APPLICATION; + } + + @Override + public Layer getLayer(Library library) { + if (library.getName().contains("SNAPSHOT.")) { + return SNAPSHOT_DEPENDENCIES; + } + return DEPENDENCIES; + } + + private boolean isClassFile(String name) { + return name.endsWith(".class"); + } + + private boolean isInResourceLocation(String name) { + for (String resourceLocation : RESOURCE_LOCATIONS) { + if (name.startsWith(resourceLocation)) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java new file mode 100644 index 0000000000..0de9bba8f0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layer.java @@ -0,0 +1,68 @@ +/* + * 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.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * A named layer used to separate the jar when creating a Docker image. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + * @see Layers + */ +public class Layer { + + private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$"); + + private final String name; + + /** + * Create a new {@link Layer} instance with the specified name. + * @param name the name of the layer. + */ + public Layer(String name) { + Assert.hasText(name, "Name must not be empty"); + Assert.isTrue(PATTERN.matcher(name).matches(), "Malformed layer name '" + name + "'"); + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.name.equals(((Layer) obj).name); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java new file mode 100644 index 0000000000..b62e65d5d7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LayeredLayout.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * A specialization of {@link RepackagingLayout} that supports layers in the repackaged + * archive. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public interface LayeredLayout extends RepackagingLayout { + + /** + * Returns the location of the layers index file that should be written or + * {@code null} if not index is required. The result should include the filename and + * is relative to the root of the jar. + * @return the layers index file location + */ + String getLayersIndexFileLocation(); + + /** + * Returns the location to which classes should be moved within the context of a + * layer. + * @param layer the destination layer for the content + * @return the repackaged classes location + */ + String getRepackagedClassesLocation(Layer layer); + + /** + * Returns the destination path for a given library within the context of a layer. + * @param libraryName the name of the library (excluding any path) + * @param scope the scope of the library + * @param layer the destination layer for the content + * @return the location of the library relative to the root of the archive (should end + * with '/') or {@code null} if the library should not be included. + */ + String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java new file mode 100644 index 0000000000..f6b438a5bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layers.java @@ -0,0 +1,57 @@ +/* + * 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.util.Iterator; + +/** + * Interface to provide information about layers to the {@link Repackager}. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + * @see Layer + */ +public interface Layers extends Iterable { + + /** + * The default layer resolver. + */ + Layers IMPLICIT = new ImplicitLayerResolver(); + + /** + * Return the jar layers in the order that they should be added (starting with the + * least frequently changed layer). + */ + @Override + Iterator iterator(); + + /** + * Return the layer that contains the given resource name. + * @param resourceName the name of the resource (for example a {@code .class} file). + * @return the layer that contains the resource (must never be {@code null}) + */ + Layer getLayer(String resourceName); + + /** + * Return the layer that contains the given library. + * @param library the library to consider + * @return the layer that contains the resource (must never be {@code null}) + */ + Layer getLayer(Library library); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 2d65ed0a91..3a9cfe3327 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -101,6 +101,28 @@ public final class Layouts { } + /** + * Executable JAR layout with support for layers. + */ + public static class LayeredJar extends Jar implements LayeredLayout { + + @Override + public String getLayersIndexFileLocation() { + return "BOOT-INF/layers.idx"; + } + + @Override + public String getRepackagedClassesLocation(Layer layer) { + return "BOOT-INF/layers/" + layer + "/classes/"; + } + + @Override + public String getLibraryLocation(String libraryName, LibraryScope scope, Layer layer) { + return "BOOT-INF/layers/" + layer + "/lib/"; + } + + } + /** * Executable expanded archive layout. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index f19d7f5ed2..bc6bb4a21c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -62,6 +62,8 @@ public class Repackager { private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index"; + private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); @@ -80,6 +82,8 @@ public class Repackager { private LayoutFactory layoutFactory; + private Layers layers = Layers.IMPLICIT; + public Repackager(File source) { this(source, null); } @@ -128,6 +132,16 @@ public class Repackager { this.layout = layout; } + /** + * Sets the layers that should be used in the jar. + * @param layers the jar layers + * @see LayeredLayout + */ + public void setLayers(Layers layers) { + Assert.notNull(layers, "Layers must not be null"); + this.layers = layers; + } + /** * Sets the layout factory for the jar. The factory can be used when no specific * layout is specified. @@ -244,7 +258,7 @@ public class Repackager { private EntryTransformer getEntityTransformer() { if (this.layout instanceof RepackagingLayout) { - return new RepackagingEntryTransformer((RepackagingLayout) this.layout); + return new RepackagingEntryTransformer((RepackagingLayout) this.layout, this.layers); } return EntryTransformer.NONE; } @@ -328,7 +342,10 @@ public class Repackager { private void addBootAttributes(Attributes attributes) { attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion()); - if (this.layout instanceof RepackagingLayout) { + if (this.layout instanceof LayeredLayout) { + addBootBootAttributesForLayeredLayout(attributes, (LayeredLayout) this.layout); + } + else if (this.layout instanceof RepackagingLayout) { addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout); } else { @@ -336,6 +353,12 @@ public class Repackager { } } + private void addBootBootAttributesForLayeredLayout(Attributes attributes, LayeredLayout layout) { + String layersIndexFileLocation = layout.getLayersIndexFileLocation(); + putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layersIndexFileLocation); + putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation()); + } + private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) { attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation()); putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE)); @@ -388,8 +411,11 @@ public class Repackager { private final RepackagingLayout layout; - private RepackagingEntryTransformer(RepackagingLayout layout) { + private final Layers layers; + + private RepackagingEntryTransformer(RepackagingLayout layout, Layers layers) { this.layout = layout; + this.layers = layers; } @Override @@ -400,7 +426,7 @@ public class Repackager { if (!isTransformable(entry)) { return entry; } - String transformedName = this.layout.getRepackagedClassesLocation() + entry.getName(); + String transformedName = transformName(entry.getName()); JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName); transformedEntry.setTime(entry.getTime()); transformedEntry.setSize(entry.getSize()); @@ -425,6 +451,15 @@ public class Repackager { return transformedEntry; } + private String transformName(String name) { + if (this.layout instanceof LayeredLayout) { + Layer layer = this.layers.getLayer(name); + Assert.state(layer != null, "Invalid 'null' layer from " + this.layers.getClass().getName()); + return ((LayeredLayout) this.layout).getRepackagedClassesLocation(layer) + name; + } + return this.layout.getRepackagedClassesLocation() + name; + } + private boolean isTransformable(JarArchiveEntry entry) { String name = entry.getName(); if (name.startsWith("META-INF/")) { @@ -456,7 +491,14 @@ public class Repackager { } private String getLocation(Library library) { - return Repackager.this.layout.getLibraryLocation(library.getName(), library.getScope()); + Layout layout = Repackager.this.layout; + if (layout instanceof LayeredLayout) { + Layers layers = Repackager.this.layers; + Layer layer = layers.getLayer(library); + Assert.state(layer != null, "Invalid 'null' library layer from " + layers.getClass().getName()); + return ((LayeredLayout) layout).getLibraryLocation(library.getName(), library.getScope(), layer); + } + return layout.getLibraryLocation(library.getName(), library.getScope()); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java new file mode 100644 index 0000000000..e4b0f4a91d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java @@ -0,0 +1,74 @@ +/* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Base class for the standard set of {@link Layers}. Defines the following layers: + *
    + *
  1. "dependencies" - For non snapshot dependencies
  2. + *
  3. "snapshot-dependencies" - For snapshot dependencies
  4. + *
  5. "resources" - For static resources such as HTML files
  6. + *
  7. "application" - For application classes and resources
  8. + *
+ * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class StandardLayers implements Layers { + + /** + * The dependencies layer. + */ + public static final Layer DEPENDENCIES = new Layer("dependencies"); + + /** + * The snapshot dependencies layer. + */ + public static final Layer SNAPSHOT_DEPENDENCIES = new Layer("snapshot-dependencies"); + + /** + * The resources layer. + */ + public static final Layer RESOURCES = new Layer("resources"); + + /** + * The application layer. + */ + public static final Layer APPLICATION = new Layer("application"); + + private static final List LAYERS; + static { + List layers = new ArrayList<>(); + layers.add(DEPENDENCIES); + layers.add(SNAPSHOT_DEPENDENCIES); + layers.add(RESOURCES); + layers.add(APPLICATION); + LAYERS = Collections.unmodifiableList(layers); + } + + @Override + public Iterator iterator() { + return LAYERS.iterator(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java new file mode 100644 index 0000000000..91794f3744 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java @@ -0,0 +1,86 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ImplicitLayerResolver}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ImplicitLayerResolverTests { + + private Layers layers = Layers.IMPLICIT; + + @Test + void iteratorReturnsLayers() { + assertThat(this.layers).containsExactly(StandardLayers.DEPENDENCIES, StandardLayers.SNAPSHOT_DEPENDENCIES, + StandardLayers.RESOURCES, StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenNameInResourceLocationReturnsResourceLayer() { + assertThat(this.layers.getLayer("META-INF/resources/logo.gif")).isEqualTo(StandardLayers.RESOURCES); + assertThat(this.layers.getLayer("resources/logo.gif")).isEqualTo(StandardLayers.RESOURCES); + assertThat(this.layers.getLayer("static/logo.gif")).isEqualTo(StandardLayers.RESOURCES); + assertThat(this.layers.getLayer("public/logo.gif")).isEqualTo(StandardLayers.RESOURCES); + } + + @Test + void getLayerWhenNameIsClassInResourceLocationReturnsApplicationLayer() { + assertThat(this.layers.getLayer("META-INF/resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("static/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("public/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenNameNotInResourceLocationReturnsApplicationLayer() { + assertThat(this.layers.getLayer("com/example/Application.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("com/example/application.properties")).isEqualTo(StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenLibraryIsSnapshotReturnsSnapshotLayer() { + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.BUILD-SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + } + + @Test + void getLayerWhenLibraryIsNotSnapshotReturnsDependenciesLayer() { + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.jar"))).isEqualTo(StandardLayers.DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-classified.jar"))) + .isEqualTo(StandardLayers.DEPENDENCIES); + } + + private Library mockLibrary(String name) { + Library library = mock(Library.class); + given(library.getName()).willReturn(name); + return library; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java new file mode 100644 index 0000000000..7be8b8b984 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java @@ -0,0 +1,62 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Layer}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class LayerTests { + + @Test + void createWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer(null)).withMessage("Name must not be empty"); + } + + @Test + void createWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("")).withMessage("Name must not be empty"); + } + + @Test + void createWhenNameContainsBadCharsThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("bad!name")) + .withMessage("Malformed layer name 'bad!name'"); + } + + @Test + void equalsAndHashCode() { + Layer layer1 = new Layer("testa"); + Layer layer2 = new Layer("testa"); + Layer layer3 = new Layer("testb"); + assertThat(layer1.hashCode()).isEqualTo(layer2.hashCode()); + assertThat(layer1).isEqualTo(layer1).isEqualTo(layer2).isNotEqualTo(layer3); + } + + @Test + void toStringReturnsName() { + assertThat(new Layer("test")).hasToString("test"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index 7add62e719..ed722952c7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -28,8 +28,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -332,6 +337,43 @@ class RepackagerTests { "BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName()); } + @Test + void layeredLayout() throws Exception { + TestJarFile libJar1 = new TestJarFile(this.tempDir); + libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile1 = libJar1.getFile(); + TestJarFile libJar2 = new TestJarFile(this.tempDir); + libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile2 = libJar2.getFile(); + TestJarFile libJar3 = new TestJarFile(this.tempDir); + libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile3 = libJar3.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + File file = this.testJarFile.getFile(); + Repackager repackager = new Repackager(file); + TestLayers layers = new TestLayers(); + layers.addLibrary(libJarFile1, "0001"); + layers.addLibrary(libJarFile2, "0002"); + layers.addLibrary(libJarFile3, "0003"); + repackager.setLayers(layers); + repackager.setLayout(new Layouts.LayeredJar()); + repackager.repackage((callback) -> { + callback.library(new Library(libJarFile1, LibraryScope.COMPILE)); + callback.library(new Library(libJarFile2, LibraryScope.COMPILE)); + callback.library(new Library(libJarFile3, LibraryScope.COMPILE)); + }); + assertThat(hasEntry(file, "BOOT-INF/classpath.idx")).isTrue(); + ZipUtil.unpack(file, new File(file.getParent())); + FileInputStream inputStream = new FileInputStream(new File(file.getParent() + "/BOOT-INF/classpath.idx")); + String index = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + String[] libraries = index.split("\\r?\\n"); + List expected = new ArrayList<>(); + expected.add("BOOT-INF/layers/0001/lib/" + libJarFile1.getName()); + expected.add("BOOT-INF/layers/0002/lib/" + libJarFile2.getName()); + expected.add("BOOT-INF/layers/0003/lib/" + libJarFile3.getName()); + assertThat(Arrays.asList(libraries)).containsExactly(expected.toArray(new String[0])); + } + @Test void duplicateLibraries() throws Exception { TestJarFile libJar = new TestJarFile(this.tempDir); @@ -746,4 +788,40 @@ class RepackagerTests { } + static class TestLayers implements Layers { + + private static final Layer DEFAULT_LAYER = new Layer("default"); + + private Set layers = new LinkedHashSet(); + + private Map libraries = new HashMap<>(); + + TestLayers() { + this.layers.add(DEFAULT_LAYER); + } + + void addLibrary(File jarFile, String layerName) { + Layer layer = new Layer(layerName); + this.layers.add(layer); + this.libraries.put(jarFile.getName(), layer); + } + + @Override + public Iterator iterator() { + return this.layers.iterator(); + } + + @Override + public Layer getLayer(String name) { + return DEFAULT_LAYER; + } + + @Override + public Layer getLayer(Library library) { + String name = new File(library.getName()).getName(); + return this.libraries.getOrDefault(name, DEFAULT_LAYER); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index c10b28e6ea..bf36fbec37 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -19,6 +19,7 @@ package org.springframework.boot.loader; import java.io.IOException; import java.util.jar.Attributes; import java.util.jar.Manifest; +import java.util.regex.Pattern; import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive.EntryFilter; @@ -36,13 +37,17 @@ import org.springframework.boot.loader.archive.ExplodedArchive; */ public class JarLauncher extends ExecutableArchiveLauncher { + private static final Pattern CLASSES_PATTERN = Pattern.compile("BOOT-INF\\/(layers\\/.*\\/)?classes/"); + + private static final Pattern LIBS_PATTERN = Pattern.compile("BOOT-INF\\/(layers\\/.*\\/)?lib\\/.+"); + private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx"; static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> { if (entry.isDirectory()) { - return entry.getName().equals("BOOT-INF/classes/"); + return CLASSES_PATTERN.matcher(entry.getName()).matches(); } - return entry.getName().startsWith("BOOT-INF/lib/"); + return LIBS_PATTERN.matcher(entry.getName()).matches(); }; public JarLauncher() {