diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 4636e5ae96ce..8f5f4fb798dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -26,7 +26,6 @@ import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -92,16 +91,15 @@ void run(PrintStream out, Map options, List parameters) try { checkJarCompatibility(); File destination = getWorkingDirectory(options); - Layers layers = getLayers(options); - Set layersToExtract = getLayersToExtract(options); - createLayersDirectories(destination, layersToExtract, layers); + FileResolver fileResolver = getFileResolver(destination, options); + fileResolver.createDirectories(); if (options.containsKey(LAUNCHER_OPTION)) { - extract(destination, layers, layersToExtract); + extractArchive(fileResolver); } else { JarStructure jarStructure = getJarStructure(); - extractLibraries(destination, layers, layersToExtract, jarStructure, options); - createRunner(destination, jarStructure, layers, layersToExtract, options); + extractLibraries(fileResolver, jarStructure, options); + createRunner(jarStructure, fileResolver, options); } } catch (IOException ex) { @@ -127,18 +125,19 @@ private void printError(PrintStream out, String message) { out.println(); } - private void extractLibraries(File destination, Layers layers, Set layersToExtract, - JarStructure jarStructure, Map options) throws IOException { - extract(destination, layers, layersToExtract, (zipEntry) -> { + private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map options) + throws IOException { + String librariesDirectory = getLibrariesDirectory(options); + extractArchive(fileResolver, (zipEntry) -> { Entry entry = jarStructure.resolve(zipEntry); if (isType(entry, Type.LIBRARY)) { - return getLibrariesDirectory(options) + entry.location(); + return librariesDirectory + entry.location(); } return null; }); } - private static Object getLibrariesDirectory(Map options) { + private static String getLibrariesDirectory(Map options) { if (options.containsKey(LIBRARIES_DIRECTORY_OPTION)) { String value = options.get(LIBRARIES_DIRECTORY_OPTION); if (value.endsWith("/")) { @@ -149,15 +148,14 @@ private static Object getLibrariesDirectory(Map options) { return "lib/"; } - private static Set getLayersToExtract(Map options) { - return StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); - } - - private Layers getLayers(Map options) { - if (options.containsKey(LAYERS_OPTION)) { - return getLayersFromContext(); + private FileResolver getFileResolver(File destination, Map options) { + String runnerFilename = getRunnerFilename(options); + if (!options.containsKey(LAYERS_OPTION)) { + return new NoLayersFileResolver(destination, runnerFilename); } - return Layers.none(); + Layers layers = getLayersFromContext(); + Set layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION)); + return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename); } private File getWorkingDirectory(Map options) { @@ -173,52 +171,12 @@ private JarStructure getJarStructure() { return jarStructure; } - private void createLayersDirectories(File directory, Set layersToExtract, Layers layers) - throws IOException { - for (String layer : layers) { - if (shouldExtractLayer(layersToExtract, layer)) { - mkDirs(new File(directory, layer)); - } - } - } - - private void write(ZipInputStream zip, ZipEntry entry, String entryName, File directory) throws IOException { - File file = new File(directory, entryName); - assertFileIsContainedInDirectory(directory, file, entry); - mkDirs(file.getParentFile()); - try (OutputStream out = new FileOutputStream(file)) { - StreamUtils.copy(zip, out); - } - try { - Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) - .setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); - } - catch (IOException ex) { - // File system does not support setting time attributes. Continue. - } - } - - private void assertFileIsContainedInDirectory(File directory, File file, ZipEntry entry) throws IOException { - String canonicalOutputPath = directory.getCanonicalPath() + File.separator; - String canonicalEntryPath = file.getCanonicalPath(); - Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), - () -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath - + "'. This is outside the output location of '" + canonicalOutputPath - + "'. Verify the contents of your archive."); - } - - private void mkDirs(File file) throws IOException { - if (!file.exists() && !file.mkdirs()) { - throw new IOException("Unable to create directory " + file); - } - } - - private void extract(File directory, Layers layers, Set layersToExtract) throws IOException { - extract(directory, layers, layersToExtract, ZipEntry::getName); + private void extractArchive(FileResolver fileResolver) throws IOException { + extractArchive(fileResolver, ZipEntry::getName); } - private void extract(File directory, Layers layers, Set layersToExtract, - EntryNameTransformer entryNameTransformer) throws IOException { + private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer) + throws IOException { withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> { if (zipEntry.isDirectory()) { return; @@ -227,21 +185,13 @@ private void extract(File directory, Layers layers, Set layersToExtract, if (name == null) { return; } - String layer = layers.getLayer(zipEntry); - if (shouldExtractLayer(layersToExtract, layer)) { - File targetDir = getLayerDirectory(directory, layer); - write(stream, zipEntry, name, targetDir); + File file = fileResolver.resolve(zipEntry, name); + if (file != null) { + extractEntry(stream, zipEntry, file); } }); } - private static File getLayerDirectory(File directory, String layer) { - if (layer == null) { - return directory; - } - return new File(directory, layer); - } - private Layers getLayersFromContext() { if (this.layers != null) { return this.layers; @@ -249,19 +199,16 @@ private Layers getLayersFromContext() { return Layers.get(this.context); } - private void createRunner(File directory, JarStructure jarStructure, Layers layers, Set layersToExtract, - Map options) throws IOException { - String runnerFileName = getRunnerFilename(options); - RunnerAwareLayers runnerAwareLayers = new RunnerAwareLayers(layers, runnerFileName, jarStructure); - String layer = runnerAwareLayers.getLayer(runnerFileName); - if (!shouldExtractLayer(layersToExtract, layer)) { + private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map options) + throws IOException { + File file = fileResolver.resolveRunner(); + if (file == null) { return; } - File targetDir = getLayerDirectory(directory, layer); - File launcherJar = new File(targetDir, runnerFileName); - Manifest manifest = jarStructure.createLauncherManifest((library) -> getLibrariesDirectory(options) + library); - mkDirs(launcherJar.getParentFile()); - try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(launcherJar.toPath()), manifest)) { + String librariesDirectory = getLibrariesDirectory(options); + Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library); + mkDirs(file.getParentFile()); + try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) { withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> { Entry entry = jarStructure.resolve(zipEntry); if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) { @@ -288,11 +235,24 @@ private static boolean isType(Entry entry, Type type) { return entry.type() == type; } - private boolean shouldExtractLayer(Set layersToExtract, String layer) { - if (layer == null || layersToExtract.isEmpty()) { - return true; + private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException { + mkDirs(file.getParentFile()); + try (OutputStream out = new FileOutputStream(file)) { + StreamUtils.copy(zip, out); + } + try { + Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) + .setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); + } + catch (IOException ex) { + // File system does not support setting time attributes. Continue. + } + } + + private static void mkDirs(File file) throws IOException { + if (!file.exists() && !file.mkdirs()) { + throw new IOException("Unable to create directory " + file); } - return layersToExtract.contains(layer); } private static JarEntry createJarEntry(String location, ZipEntry originalEntry) { @@ -324,6 +284,15 @@ private static void withZipEntries(File file, ThrowingConsumer callback) throws } } + private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException { + String canonicalOutputPath = directory.getCanonicalPath() + File.separator; + String canonicalEntryPath = file.getCanonicalPath(); + Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), + () -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." + .formatted(name, canonicalEntryPath, canonicalOutputPath)); + return file; + } + @FunctionalInterface private interface EntryNameTransformer { @@ -338,31 +307,128 @@ private interface ThrowingConsumer { } - private static final class RunnerAwareLayers implements Layers { + private interface FileResolver { + + /** + * Creates needed directories. + * @throws IOException if something went wrong + */ + void createDirectories() throws IOException; + + /** + * Resolves the given {@link ZipEntry} to a file. + * @param entry the zip entry + * @param newName the new name of the file + * @return file where the contents should be written or {@code null} if this entry + * should be skipped + * @throws IOException if something went wrong + */ + default File resolve(ZipEntry entry, String newName) throws IOException { + return resolve(entry.getName(), newName); + } - private final Layers layers; + /** + * Resolves the given name to a file. + * @param originalName the original name of the file + * @param newName the new name of the file + * @return file where the contents should be written or {@code null} if this name + * should be skipped + * @throws IOException if something went wrong + */ + File resolve(String originalName, String newName) throws IOException; + + /** + * Resolves the file for the runner. + * @return the file for the runner or {@code null} if the runner should be skipped + * @throws IOException if something went wrong + */ + File resolveRunner() throws IOException; + + } + + private static final class NoLayersFileResolver implements FileResolver { + + private final File directory; private final String runnerFilename; - private final JarStructure jarStructure; + private NoLayersFileResolver(File directory, String runnerFilename) { + this.directory = directory; + this.runnerFilename = runnerFilename; + } + + @Override + public void createDirectories() { + } + + @Override + public File resolve(String originalName, String newName) throws IOException { + return assertFileIsContainedInDirectory(this.directory, new File(this.directory, newName), newName); + } + + @Override + public File resolveRunner() throws IOException { + return resolve(this.runnerFilename, this.runnerFilename); + } + + } + + private static final class LayersFileResolver implements FileResolver { + + private final Layers layers; + + private final Set layersToExtract; + + private final File directory; - RunnerAwareLayers(Layers layers, String runnerFilename, JarStructure jarStructure) { + private final String runnerFilename; + + LayersFileResolver(File directory, Layers layers, Set layersToExtract, String runnerFilename) { this.layers = layers; + this.layersToExtract = layersToExtract; + this.directory = directory; this.runnerFilename = runnerFilename; - this.jarStructure = jarStructure; } @Override - public Iterator iterator() { - return this.layers.iterator(); + public void createDirectories() throws IOException { + for (String layer : this.layers) { + if (shouldExtractLayer(layer)) { + mkDirs(getLayerDirectory(layer)); + } + } + } + + @Override + public File resolve(String originalName, String newName) throws IOException { + String layer = this.layers.getLayer(originalName); + if (shouldExtractLayer(layer)) { + File directory = getLayerDirectory(layer); + return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName); + } + return null; } @Override - public String getLayer(String entryName) { - if (this.runnerFilename.equals(entryName)) { - return this.layers.getLayer(this.jarStructure.getClassesLocation()); + public File resolveRunner() throws IOException { + String layer = this.layers.getApplicationLayerName(); + if (shouldExtractLayer(layer)) { + File directory = getLayerDirectory(layer); + return assertFileIsContainedInDirectory(directory, new File(directory, this.runnerFilename), + this.runnerFilename); + } + return null; + } + + private File getLayerDirectory(String layer) { + return new File(this.directory, layer); + } + + private boolean shouldExtractLayer(String layer) { + if (this.layersToExtract.isEmpty()) { + return true; } - return this.layers.getLayer(entryName); + return this.layersToExtract.contains(layer); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java index 4a4cab9089b3..0469aeed8363 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java @@ -39,12 +39,16 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Moritz Halbritter */ class IndexedLayers implements Layers { private final Map> layers = new LinkedHashMap<>(); - IndexedLayers(String indexFile) { + private final String classesLocation; + + IndexedLayers(String indexFile, String classesLocation) { + this.classesLocation = classesLocation; String[] lines = Arrays.stream(indexFile.split("\n")) .map((line) -> line.replace("\r", "")) .filter(StringUtils::hasText) @@ -66,6 +70,11 @@ else if (line.startsWith(" - ")) { Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); } + @Override + public String getApplicationLayerName() { + return getLayer(this.classesLocation); + } + @Override public Iterator iterator() { return this.layers.keySet().iterator(); @@ -97,7 +106,8 @@ static IndexedLayers get(Context context) { ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null; if (entry != null) { String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8); - return new IndexedLayers(indexFile); + String classesLocation = manifest.getMainAttributes().getValue("Spring-Boot-Classes"); + return new IndexedLayers(indexFile, classesLocation); } } return null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java index 0fa75183e4a5..05eb07ebdc90 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java @@ -16,7 +16,6 @@ package org.springframework.boot.jarmode.tools; -import java.util.Collections; import java.util.Iterator; import java.util.zip.ZipEntry; @@ -53,6 +52,12 @@ default String getLayer(ZipEntry entry) { */ String getLayer(String entryName); + /** + * Return the name of the application layer. + * @return the name of the application layer + */ + String getApplicationLayerName(); + /** * Return a {@link Layers} instance for the currently running application. * @param context the command context @@ -67,20 +72,6 @@ static Layers get(Context context) { return indexedLayers; } - static Layers none() { - return new Layers() { - @Override - public Iterator iterator() { - return Collections.emptyIterator(); - } - - @Override - public String getLayer(String entryName) { - return null; - } - }; - } - final class LayersNotEnabledException extends RuntimeException { LayersNotEnabledException() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index 3229b428bb30..bf77b76a0547 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -257,6 +257,11 @@ public String getLayer(String entryName) { return "c"; } + @Override + public String getApplicationLayerName() { + return "application"; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java index b93a447817c0..9c543ca6b256 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java @@ -46,46 +46,46 @@ class IndexedLayersTests { @Test void createWhenIndexFileIsEmptyThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ")) + assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ", "BOOT-INF/classes")) .withMessage("Empty layer index file loaded"); } @Test void createWhenIndexFileIsMalformedThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test")) + assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test", "BOOT-INF/classes")) .withMessage("Layer index file is malformed"); } @Test void iteratorReturnsLayers() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers).containsExactly("test", "empty", "application"); } @Test void getLayerWhenMatchesNameReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test"); assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application"); } @Test void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar"))) .withMessage("No layer defined in index for file " + "'file.jar'"); } @Test void getLayerWhenMatchesDirectoryReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application"); assertThat(layers.getLayer(mockEntry("META-INF/a/sub/directory/and/a/file"))).isEqualTo("application"); } @Test void getLayerWhenFileHasSpaceReturnsLayer() throws Exception { - IndexedLayers layers = new IndexedLayers(getIndex()); + IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application"); }