diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java index afc4d2e12a73..120d26d004ea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -56,12 +56,14 @@ */ public class JarWriter implements LoaderClassesWriter, AutoCloseable { - private static final UnpackHandler NEVER_UNPACK = new NeverUnpackHandler(); - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; private static final int BUFFER_SIZE = 32 * 1024; + private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; + + private static final int UNIX_DIR_MODE = UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM; + private final JarArchiveOutputStream jarOutput; private final Set writtenEntries = new HashSet<>(); @@ -121,11 +123,11 @@ public void writeManifest(Manifest manifest) throws IOException { * @throws IOException if the entries cannot be written */ public void writeEntries(JarFile jarFile) throws IOException { - this.writeEntries(jarFile, new IdentityEntryTransformer(), NEVER_UNPACK); + this.writeEntries(jarFile, EntryTransformer.NONE, UnpackHandler.NEVER); } void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException { - this.writeEntries(jarFile, new IdentityEntryTransformer(), unpackHandler); + this.writeEntries(jarFile, EntryTransformer.NONE, unpackHandler); } void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler) @@ -244,7 +246,7 @@ public void close() throws IOException { } private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException { - writeEntry(entry, entryWriter, NEVER_UNPACK); + writeEntry(entry, entryWriter, UnpackHandler.NEVER); } /** @@ -257,22 +259,10 @@ private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws I */ private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler) throws IOException { - String parent = entry.getName(); - if (parent.endsWith("/")) { - parent = parent.substring(0, parent.length() - 1); - entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); - } - else { - entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); - } - if (parent.lastIndexOf('/') != -1) { - parent = parent.substring(0, parent.lastIndexOf('/') + 1); - if (!parent.isEmpty()) { - writeEntry(new JarArchiveEntry(parent), null, unpackHandler); - } - } - - if (this.writtenEntries.add(entry.getName())) { + String name = entry.getName(); + writeParentFolderEntries(name); + if (this.writtenEntries.add(name)) { + entry.setUnixMode(name.endsWith("/") ? UNIX_DIR_MODE : UNIX_FILE_MODE); entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler); this.jarOutput.putArchiveEntry(entry); if (entryWriter != null) { @@ -282,6 +272,16 @@ private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHa } } + private void writeParentFolderEntries(String name) throws IOException { + String parent = name.endsWith("/") ? name.substring(0, name.length() - 1) : name; + while (parent.lastIndexOf('/') != -1) { + parent = parent.substring(0, parent.lastIndexOf('/')); + if (!parent.isEmpty()) { + writeEntry(new JarArchiveEntry(parent + "/"), null, UnpackHandler.NEVER); + } + } + } + private EntryWriter addUnpackCommentIfNecessary(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler) throws IOException { if (entryWriter == null || !unpackHandler.requiresUnpack(entry.getName())) { @@ -444,21 +444,15 @@ void setupStoredEntry(JarArchiveEntry entry) { * An {@code EntryTransformer} enables the transformation of {@link JarEntry jar * entries} during the writing process. */ + @FunctionalInterface interface EntryTransformer { - JarArchiveEntry transform(JarArchiveEntry jarEntry); - - } - - /** - * An {@code EntryTransformer} that returns the entry unchanged. - */ - private static final class IdentityEntryTransformer implements EntryTransformer { + /** + * No-op entity transformer. + */ + EntryTransformer NONE = (jarEntry) -> jarEntry; - @Override - public JarArchiveEntry transform(JarArchiveEntry jarEntry) { - return jarEntry; - } + JarArchiveEntry transform(JarArchiveEntry jarEntry); } @@ -468,23 +462,23 @@ public JarArchiveEntry transform(JarArchiveEntry jarEntry) { */ interface UnpackHandler { - boolean requiresUnpack(String name); + UnpackHandler NEVER = new UnpackHandler() { - String sha1Hash(String name) throws IOException; + @Override + public boolean requiresUnpack(String name) { + return false; + } - } + @Override + public String sha1Hash(String name) throws IOException { + throw new UnsupportedOperationException(); + } - private static final class NeverUnpackHandler implements UnpackHandler { + }; - @Override - public boolean requiresUnpack(String name) { - return false; - } + boolean requiresUnpack(String name); - @Override - public String sha1Hash(String name) { - throw new UnsupportedOperationException(); - } + String sha1Hash(String name) throws IOException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java index f7e7e938c4df..0634f8dff311 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -35,13 +35,26 @@ public interface Layout { */ String getLauncherClassName(); + /** + * Returns the destination path for a given library. + * @param libraryName the name of the library (excluding any path) + * @param scope the scope of the library + * @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. + */ + default String getLibraryLocation(String libraryName, LibraryScope scope) { + return getLibraryDestination(libraryName, scope); + } + /** * Returns the destination path for a given library. * @param libraryName the name of the library (excluding any path) * @param scope the scope of the library * @return the destination relative to the root of the archive (should end with '/') * or {@code null} if the library should not be included. + * @deprecated since 2.3.0 in favor of {@link #getLibraryLocation} */ + @Deprecated String getLibraryDestination(String libraryName, LibraryScope scope); /** 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 fed158d289ac..6f15777ebe0d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -67,6 +67,12 @@ public String getLauncherClassName() { return "org.springframework.boot.loader.JarLauncher"; } + @Override + public String getLibraryLocation(String libraryName, LibraryScope scope) { + return "BOOT-INF/lib/"; + } + + @Deprecated @Override public String getLibraryDestination(String libraryName, LibraryScope scope) { return "BOOT-INF/lib/"; @@ -123,15 +129,15 @@ public boolean isExecutable() { */ public static class War implements Layout { - private static final Map SCOPE_DESTINATIONS; + private static final Map SCOPE_LOCATION; static { - Map map = new HashMap<>(); - map.put(LibraryScope.COMPILE, "WEB-INF/lib/"); - map.put(LibraryScope.CUSTOM, "WEB-INF/lib/"); - map.put(LibraryScope.RUNTIME, "WEB-INF/lib/"); - map.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/"); - SCOPE_DESTINATIONS = Collections.unmodifiableMap(map); + Map locations = new HashMap<>(); + locations.put(LibraryScope.COMPILE, "WEB-INF/lib/"); + locations.put(LibraryScope.CUSTOM, "WEB-INF/lib/"); + locations.put(LibraryScope.RUNTIME, "WEB-INF/lib/"); + locations.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/"); + SCOPE_LOCATION = Collections.unmodifiableMap(locations); } @Override @@ -139,9 +145,15 @@ public String getLauncherClassName() { return "org.springframework.boot.loader.WarLauncher"; } + @Override + public String getLibraryLocation(String libraryName, LibraryScope scope) { + return SCOPE_LOCATION.get(scope); + } + + @Deprecated @Override public String getLibraryDestination(String libraryName, LibraryScope scope) { - return SCOPE_DESTINATIONS.get(scope); + return SCOPE_LOCATION.get(scope); } @Override 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 281208082d56..08a1499860e1 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 @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; +import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -54,10 +55,10 @@ public class Repackager { private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version"; - private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; - private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes"; + private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; + private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); @@ -81,13 +82,9 @@ public Repackager(File source) { } public Repackager(File source, LayoutFactory layoutFactory) { - if (source == null) { - throw new IllegalArgumentException("Source file must be provided"); - } - if (!source.exists() || !source.isFile()) { - throw new IllegalArgumentException( - "Source must refer to an existing file, got " + source.getAbsolutePath()); - } + Assert.notNull(source, "Source file must be provided"); + Assert.isTrue(source.exists() && source.isFile(), + "Source must refer to an existing file, got " + source.getAbsolutePath()); this.source = source.getAbsoluteFile(); this.layoutFactory = layoutFactory; } @@ -124,9 +121,7 @@ public void setBackupSource(boolean backupSource) { * @param layout the layout */ public void setLayout(Layout layout) { - if (layout == null) { - throw new IllegalArgumentException("Layout must not be null"); - } + Assert.notNull(layout, "Layout must not be null"); this.layout = layout; } @@ -169,12 +164,8 @@ public void repackage(File destination, Libraries libraries) throws IOException * @since 1.3.0 */ public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException { - if (destination == null || destination.isDirectory()) { - throw new IllegalArgumentException("Invalid destination"); - } - if (libraries == null) { - throw new IllegalArgumentException("Libraries must not be null"); - } + Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination"); + Assert.notNull(libraries, "Libraries must not be null"); if (this.layout == null) { this.layout = getLayoutFactory().getLayout(this.source); } @@ -234,14 +225,7 @@ private void repackage(JarFile sourceJar, File destination, Libraries libraries, try (JarWriter writer = new JarWriter(destination, launchScript)) { writer.writeManifest(buildManifest(sourceJar)); writeLoaderClasses(writer); - if (this.layout instanceof RepackagingLayout) { - writer.writeEntries(sourceJar, - new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()), - writeableLibraries); - } - else { - writer.writeEntries(sourceJar, writeableLibraries); - } + writer.writeEntries(sourceJar, getEntityTransformer(), writeableLibraries); writeableLibraries.write(writer); } } @@ -255,6 +239,13 @@ else if (this.layout.isExecutable()) { } } + private EntryTransformer getEntityTransformer() { + if (this.layout instanceof RepackagingLayout) { + return new RepackagingEntryTransformer((RepackagingLayout) this.layout); + } + return EntryTransformer.NONE; + } + private boolean isZip(File file) { try { try (FileInputStream fileInputStream = new FileInputStream(file)) { @@ -276,39 +267,43 @@ private boolean isZip(InputStream inputStream) throws IOException { } private Manifest buildManifest(JarFile source) throws IOException { - Manifest manifest = source.getManifest(); - if (manifest == null) { - manifest = new Manifest(); - manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); - } - manifest = new Manifest(manifest); - String startClass = this.mainClass; - if (startClass == null) { - startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); - } - if (startClass == null) { - startClass = findMainMethodWithTimeoutWarning(source); - } - String launcherClassName = this.layout.getLauncherClassName(); - if (launcherClassName != null) { - manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName); - if (startClass == null) { - throw new IllegalStateException("Unable to find main class"); - } - manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass); + Manifest manifest = createInitialManifest(source); + addMainAndStartAttributes(source, manifest); + addBootAttributes(manifest.getMainAttributes()); + return manifest; + } + + private Manifest createInitialManifest(JarFile source) throws IOException { + if (source.getManifest() != null) { + return new Manifest(source.getManifest()); } - else if (startClass != null) { - manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + return manifest; + } + + private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException { + String mainClass = getMainClass(source, manifest); + String launcherClass = this.layout.getLauncherClassName(); + if (launcherClass != null) { + Assert.state(mainClass != null, "Unable to find main class"); + manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass); + manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass); } - String bootVersion = getClass().getPackage().getImplementationVersion(); - manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion); - manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout) - ? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation()); - String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE); - if (StringUtils.hasLength(lib)) { - manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib); + else if (mainClass != null) { + manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass); } - return manifest; + } + + private String getMainClass(JarFile source, Manifest manifest) throws IOException { + if (this.mainClass != null) { + return this.mainClass; + } + String attributeValue = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); + if (attributeValue != null) { + return attributeValue; + } + return findMainMethodWithTimeoutWarning(source); } private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException { @@ -328,6 +323,32 @@ protected String findMainMethod(JarFile source) throws IOException { SPRING_BOOT_APPLICATION_CLASS_NAME); } + private void addBootAttributes(Attributes attributes) { + attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion()); + if (this.layout instanceof RepackagingLayout) { + addBootBootAttributesForRepackagingLayout(attributes, (RepackagingLayout) this.layout); + } + else { + addBootBootAttributesForPlainLayout(attributes, this.layout); + } + } + + private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) { + attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation()); + putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE)); + } + + private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) { + attributes.putValue(BOOT_CLASSES_ATTRIBUTE, this.layout.getClassesLocation()); + putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE)); + } + + private void putIfHasLength(Attributes attributes, String name, String value) { + if (StringUtils.hasLength(value)) { + attributes.putValue(name, value); + } + } + private void renameFile(File file, File dest) { if (!file.renameTo(dest)) { throw new IllegalStateException("Unable to rename '" + file + "' to '" + dest + "'"); @@ -359,12 +380,12 @@ public interface MainClassTimeoutWarningListener { /** * An {@code EntryTransformer} that renames entries by applying a prefix. */ - private static final class RenamingEntryTransformer implements EntryTransformer { + private static final class RepackagingEntryTransformer implements EntryTransformer { - private final String namePrefix; + private final RepackagingLayout layout; - private RenamingEntryTransformer(String namePrefix) { - this.namePrefix = namePrefix; + private RepackagingEntryTransformer(RepackagingLayout layout) { + this.layout = layout; } @Override @@ -372,33 +393,40 @@ public JarArchiveEntry transform(JarArchiveEntry entry) { if (entry.getName().equals("META-INF/INDEX.LIST")) { return null; } - if ((entry.getName().startsWith("META-INF/") && !entry.getName().equals("META-INF/aop.xml") - && !entry.getName().endsWith(".kotlin_module")) || entry.getName().startsWith("BOOT-INF/") - || entry.getName().equals("module-info.class")) { + if (!isTransformable(entry)) { return entry; } - JarArchiveEntry renamedEntry = new JarArchiveEntry(this.namePrefix + entry.getName()); - renamedEntry.setTime(entry.getTime()); - renamedEntry.setSize(entry.getSize()); - renamedEntry.setMethod(entry.getMethod()); + String transformedName = this.layout.getRepackagedClassesLocation() + entry.getName(); + JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName); + transformedEntry.setTime(entry.getTime()); + transformedEntry.setSize(entry.getSize()); + transformedEntry.setMethod(entry.getMethod()); if (entry.getComment() != null) { - renamedEntry.setComment(entry.getComment()); + transformedEntry.setComment(entry.getComment()); } - renamedEntry.setCompressedSize(entry.getCompressedSize()); - renamedEntry.setCrc(entry.getCrc()); + transformedEntry.setCompressedSize(entry.getCompressedSize()); + transformedEntry.setCrc(entry.getCrc()); if (entry.getCreationTime() != null) { - renamedEntry.setCreationTime(entry.getCreationTime()); + transformedEntry.setCreationTime(entry.getCreationTime()); } if (entry.getExtra() != null) { - renamedEntry.setExtra(entry.getExtra()); + transformedEntry.setExtra(entry.getExtra()); } if (entry.getLastAccessTime() != null) { - renamedEntry.setLastAccessTime(entry.getLastAccessTime()); + transformedEntry.setLastAccessTime(entry.getLastAccessTime()); } if (entry.getLastModifiedTime() != null) { - renamedEntry.setLastModifiedTime(entry.getLastModifiedTime()); + transformedEntry.setLastModifiedTime(entry.getLastModifiedTime()); + } + return transformedEntry; + } + + private boolean isTransformable(JarArchiveEntry entry) { + String name = entry.getName(); + if (name.startsWith("META-INF/")) { + return name.equals("META-INF/aop.xml") || name.endsWith(".kotlin_module"); } - return renamedEntry; + return !name.startsWith("BOOT-INF/") && !name.equals("module-info.class"); } } @@ -414,19 +442,19 @@ private final class WritableLibraries implements UnpackHandler { private WritableLibraries(Libraries libraries) throws IOException { libraries.doWithLibraries((library) -> { if (isZip(library.getFile())) { - String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(), - library.getScope()); - if (libraryDestination != null) { - Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(), - library); - if (existing != null) { - throw new IllegalStateException("Duplicate library " + library.getName()); - } + String location = getLocation(library); + if (location != null) { + Library existing = this.libraryEntryNames.putIfAbsent(location + library.getName(), library); + Assert.state(existing == null, "Duplicate library " + library.getName()); } } }); } + private String getLocation(Library library) { + return Repackager.this.layout.getLibraryLocation(library.getName(), library.getScope()); + } + @Override public boolean requiresUnpack(String name) { Library library = this.libraryEntryNames.get(name); @@ -436,9 +464,7 @@ public boolean requiresUnpack(String name) { @Override public String sha1Hash(String name) throws IOException { Library library = this.libraryEntryNames.get(name); - if (library == null) { - throw new IllegalArgumentException("No library found for entry name '" + name + "'"); - } + Assert.notNull(library, "No library found for entry name '" + name + "'"); return FileUtils.sha1Hash(library.getFile()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java index 6145db5e2215..52ef2e61441c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -56,19 +56,19 @@ void unknownFile() { @Test void jarLayout() { Layout layout = new Layouts.Jar(); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/"); } @Test void warLayout() { Layout layout = new Layouts.War(); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/"); } } 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 11a8aab4b16d..f52cfb5e3317 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -324,8 +324,8 @@ void customLayout() throws Exception { Layout layout = mock(Layout.class); LibraryScope scope = mock(LibraryScope.class); given(layout.getLauncherClassName()).willReturn("testLauncher"); - given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/"); - given(layout.getLibraryDestination(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/"); + given(layout.getLibraryLocation(anyString(), eq(scope))).willReturn("test/"); + given(layout.getLibraryLocation(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/"); repackager.setLayout(layout); repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope))); assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java index 307211a200cb..92ff59154c1b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -32,6 +32,8 @@ */ public abstract class ExecutableArchiveLauncher extends Launcher { + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + private final Archive archive; public ExecutableArchiveLauncher() { @@ -44,11 +46,12 @@ public ExecutableArchiveLauncher() { } protected ExecutableArchiveLauncher(Archive archive) { - this.archive = archive; - } - - protected final Archive getArchive() { - return this.archive; + try { + this.archive = archive; + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } } @Override @@ -56,7 +59,7 @@ protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { - mainClass = manifest.getMainAttributes().getValue("Start-Class"); + mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); @@ -125,4 +128,12 @@ protected boolean supportsNestedJars() { return this.archive.supportsNestedJars(); } + /** + * Return the root archive. + * @return the root archive + */ + protected final Archive getArchive() { + return this.archive; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java index 7b100acf3b2f..5fc0a0fb43a2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -77,7 +77,9 @@ protected ClassLoader createClassLoader(List archives) throws Exception protected ClassLoader createClassLoader(Iterator archives) throws Exception { List urls = new ArrayList<>(50); while (archives.hasNext()) { - urls.add(archives.next().getUrl()); + Archive archive = archives.next(); + urls.add(archive.getUrl()); + archive.close(); } return createClassLoader(urls.toArray(new URL[0])); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java index 49449cf24380..475a75495de0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -23,7 +23,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; @@ -53,7 +53,15 @@ protected File createJarArchive(String name, String entryPrefix) throws IOExcept jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/")); - JarEntry libFoo = new JarEntry(entryPrefix + "/lib/foo.jar"); + addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream); + jarOutputStream.close(); + return archive; + } + + private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException { + JarEntry libFoo = new JarEntry(entryPrefix + lib); libFoo.setMethod(ZipEntry.STORED); ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream(); new JarOutputStream(fooJarStream).close(); @@ -63,8 +71,6 @@ protected File createJarArchive(String name, String entryPrefix) throws IOExcept libFoo.setCrc(crc32.getValue()); jarOutputStream.putNextEntry(libFoo); jarOutputStream.write(fooJarStream.toByteArray()); - jarOutputStream.close(); - return archive; } protected File explode(File archive) throws IOException { @@ -87,11 +93,20 @@ protected File explode(File archive) throws IOException { } protected Set getUrls(List archives) throws MalformedURLException { - Set urls = new HashSet<>(archives.size()); + Set urls = new LinkedHashSet<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return urls; } + protected final URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java index 35f148cbf335..2bebbdde0641 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -42,9 +42,7 @@ void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); List archives = new ArrayList<>(); launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).hasSize(2); - assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(), - new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL()); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); for (Archive archive : archives) { archive.close(); } @@ -57,14 +55,29 @@ void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws JarLauncher launcher = new JarLauncher(archive); List classPathArchives = new ArrayList<>(); launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); - assertThat(classPathArchives).hasSize(2); + assertThat(classPathArchives).hasSize(4); assertThat(getUrls(classPathArchives)).containsOnly( new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/")); + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/")); for (Archive classPathArchive : classPathArchives) { classPathArchive.close(); } } } + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java index 0bdb8ce0ba9a..b0d5539ddfec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -42,29 +42,41 @@ void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws E WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); List archives = new ArrayList<>(); launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).hasSize(2); - assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "WEB-INF/classes").toURI().toURL(), - new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL()); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); for (Archive archive : archives) { archive.close(); } } @Test - void archivedWarHasOnlyWebInfClassesAndContentsOWebInfLibOnClasspath() throws Exception { + void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { File jarRoot = createJarArchive("archive.war", "WEB-INF"); try (JarFileArchive archive = new JarFileArchive(jarRoot)) { WarLauncher launcher = new WarLauncher(archive); List classPathArchives = new ArrayList<>(); launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); - assertThat(classPathArchives).hasSize(2); assertThat(getUrls(classPathArchives)).containsOnly( new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/")); + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/")); for (Archive classPathArchive : classPathArchives) { classPathArchive.close(); } } } + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + }