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 120d26d004ea..b54daf7a22f9 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 @@ -17,6 +17,7 @@ package org.springframework.boot.loader.tools; import java.io.BufferedInputStream; +import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -27,13 +28,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -52,6 +56,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public class JarWriter implements LoaderClassesWriter, AutoCloseable { @@ -190,6 +195,28 @@ public void writeNestedLibrary(String destination, Library library) throws IOExc } } + /** + * Write a simple index file containing the specified UTF-8 lines. + * @param location the location of the index file + * @param lines the lines to write + * @throws IOException if the write fails + * @since 2.3.0 + */ + public void writeIndexFile(String location, List lines) throws IOException { + if (location != null) { + JarArchiveEntry entry = new JarArchiveEntry(location); + writeEntry(entry, (outputStream) -> { + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + for (String line : lines) { + writer.write(line); + writer.write("\n"); + } + writer.flush(); + }); + } + } + private long getNestedLibraryTime(File file) { try { try (JarFile jarFile = new JarFile(file)) { 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 6f15777ebe0d..2d65ed0a9102 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 @@ -28,6 +28,7 @@ * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public final class Layouts { @@ -88,6 +89,11 @@ public String getRepackagedClassesLocation() { return "BOOT-INF/classes/"; } + @Override + public String getClasspathIndexFileLocation() { + return "BOOT-INF/classpath.idx"; + } + @Override public boolean isExecutable() { return true; 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 08a1499860e1..f19d7f5ed2fa 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 @@ -45,6 +45,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Madhura Bhave * @since 1.0.0 */ public class Repackager { @@ -59,6 +60,8 @@ public class Repackager { private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; + private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-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); @@ -336,6 +339,7 @@ private void addBootAttributes(Attributes attributes) { private void addBootBootAttributesForRepackagingLayout(Attributes attributes, RepackagingLayout layout) { attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getRepackagedClassesLocation()); putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, this.layout.getLibraryLocation("", LibraryScope.COMPILE)); + putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation()); } private void addBootBootAttributesForPlainLayout(Attributes attributes, Layout layout) { @@ -473,6 +477,10 @@ private void write(JarWriter writer) throws IOException { writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1), entry.getValue()); } + if (Repackager.this.layout instanceof RepackagingLayout) { + String location = ((RepackagingLayout) (Repackager.this.layout)).getClasspathIndexFileLocation(); + writer.writeIndexFile(location, new ArrayList<>(this.libraryEntryNames.keySet())); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java index 953e0864d2bc..e52388949289 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.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. @@ -31,4 +31,15 @@ public interface RepackagingLayout extends Layout { */ String getRepackagedClassesLocation(); + /** + * Returns the location of the classpath 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 classpath index file location + * @since 2.3.0 + */ + default String getClasspathIndexFileLocation() { + return null; + } + } 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 f52cfb5e3317..7add62e71933 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 @@ -18,11 +18,14 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Enumeration; import java.util.List; @@ -45,6 +48,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithMainMethod; import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -59,6 +63,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave */ class RepackagerTests { @@ -299,6 +304,34 @@ void libraries() throws Exception { assertThat(entry.getComment()).hasSize(47); } + @Test + void index() 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); + 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"); + assertThat(Arrays.asList(libraries)).contains("BOOT-INF/lib/" + libJarFile1.getName(), + "BOOT-INF/lib/" + libJarFile2.getName(), "BOOT-INF/lib/" + libJarFile3.getName()); + } + @Test void duplicateLibraries() throws Exception { TestJarFile libJar = new TestJarFile(this.tempDir); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java new file mode 100644 index 000000000000..b095a85e512f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -0,0 +1,130 @@ +/* + * 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; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A class path index file that provides ordering information for JARs. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +final class ClassPathIndexFile { + + private final File root; + + private final List lines; + + private final Set folders; + + private ClassPathIndexFile(File root, List lines) { + this.root = root; + this.lines = lines; + this.folders = this.lines.stream().map(this::getFolder).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + private String getFolder(String name) { + int lastSlash = name.lastIndexOf("/"); + return (lastSlash != -1) ? name.substring(0, lastSlash) : null; + } + + int size() { + return this.lines.size(); + } + + boolean containsFolder(String name) { + if (name == null || name.isEmpty()) { + return false; + } + if (name.endsWith("/")) { + return containsFolder(name.substring(0, name.length() - 1)); + } + return this.folders.contains(name); + } + + List getUrls() { + return Collections.unmodifiableList(this.lines.stream().map(this::asUrl).collect(Collectors.toList())); + } + + private URL asUrl(String line) { + try { + return new File(this.root, line).toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException { + return loadIfPossible(asFile(root), location); + } + + private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { + return loadIfPossible(root, new File(root, location)); + } + + private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException { + if (indexFile.exists() && indexFile.isFile()) { + try (InputStream inputStream = new FileInputStream(indexFile)) { + return new ClassPathIndexFile(root, loadLines(inputStream)); + } + } + return null; + } + + private static List loadLines(InputStream inputStream) throws IOException { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = reader.readLine(); + while (line != null) { + if (!line.trim().isEmpty()) { + lines.add(line); + } + line = reader.readLine(); + } + return Collections.unmodifiableList(lines); + } + + private static File asFile(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException("URL does not reference a file"); + } + try { + return new File(url.toURI()); + } + catch (URISyntaxException ex) { + return new File(url.getPath()); + } + } + +} 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 92ff59154c1b..11a1e7a4e7e9 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 @@ -16,6 +16,8 @@ package org.springframework.boot.loader; +import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -28,17 +30,23 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public abstract class ExecutableArchiveLauncher extends Launcher { private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + private final Archive archive; + private final ClassPathIndexFile classPathIndex; + public ExecutableArchiveLauncher() { try { this.archive = createArchive(); + this.classPathIndex = getClassPathIndex(this.archive); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -48,12 +56,17 @@ public ExecutableArchiveLauncher() { protected ExecutableArchiveLauncher(Archive archive) { try { this.archive = archive; + this.classPathIndex = getClassPathIndex(this.archive); } catch (Exception ex) { throw new IllegalStateException(ex); } } + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + return null; + } + @Override protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); @@ -67,15 +80,42 @@ protected String getMainClass() throws Exception { return mainClass; } + @Override + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(guessClassPathSize()); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + if (this.classPathIndex != null) { + urls.addAll(this.classPathIndex.getUrls()); + } + return super.createClassLoader(urls.toArray(new URL[0])); + } + + private int guessClassPathSize() { + if (this.classPathIndex != null) { + return this.classPathIndex.size() + 10; + } + return 50; + } + @Override protected Iterator getClassPathArchivesIterator() throws Exception { - Iterator archives = this.archive.getNestedArchives(this::isSearchCandidate, this::isNestedArchive); + Archive.EntryFilter searchFilter = (entry) -> isSearchCandidate(entry) && !isFolderIndexed(entry); + Iterator archives = this.archive.getNestedArchives(searchFilter, this::isNestedArchive); if (isPostProcessingClassPathArchives()) { archives = applyClassPathArchivePostProcessing(archives); } return archives; } + private boolean isFolderIndexed(Archive.Entry entry) { + if (this.classPathIndex != null) { + return this.classPathIndex.containsFolder(entry.getName()); + } + return false; + } + private Iterator applyClassPathArchivePostProcessing(Iterator archives) throws Exception { List list = new ArrayList<>(); while (archives.hasNext()) { 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 bcb6c75fdaa0..c10b28e6ea5d 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 @@ -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. @@ -16,8 +16,13 @@ package org.springframework.boot.loader; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.Archive.EntryFilter; +import org.springframework.boot.loader.archive.ExplodedArchive; /** * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are @@ -26,10 +31,13 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public class JarLauncher extends ExecutableArchiveLauncher { + 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/"); @@ -44,6 +52,23 @@ protected JarLauncher(Archive archive) { super(archive); } + @Override + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + // Only needed for exploded archives, regular ones already have a defined order + if (archive instanceof ExplodedArchive) { + String location = getClassPathIndexFileLocation(archive); + return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); + } + return super.getClassPathIndex(archive); + } + + private String getClassPathIndexFileLocation(Archive archive) throws IOException { + Manifest manifest = archive.getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; + return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION; + } + @Override protected boolean isPostProcessingClassPathArchives() { return false; 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 475a75495de0..13f0bdea98eb 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 @@ -20,8 +20,11 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; @@ -41,6 +44,7 @@ * Base class for testing {@link ExecutableArchiveLauncher} implementations. * * @author Andy Wilkinson + * @author Madhura Bhave */ public abstract class AbstractExecutableArchiveLauncherTests { @@ -48,11 +52,25 @@ public abstract class AbstractExecutableArchiveLauncherTests { File tempDir; protected File createJarArchive(String name, String entryPrefix) throws IOException { + return createJarArchive(name, entryPrefix, false); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, String entryPrefix, boolean indexed) throws IOException { File archive = new File(this.tempDir, name); JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(archive)); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/")); + if (indexed) { + JarEntry indexEntry = new JarEntry(entryPrefix + "/classpath.idx"); + jarOutputStream.putNextEntry(indexEntry); + Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); + writer.write("BOOT-INF/lib/foo.jar\n"); + writer.write("BOOT-INF/lib/bar.jar\n"); + writer.write("BOOT-INF/lib/baz.jar\n"); + writer.flush(); + } addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream); addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream); addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java new file mode 100644 index 000000000000..2510c9e7cf45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java @@ -0,0 +1,123 @@ +/* + * 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; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ClassPathIndexFile}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ClassPathIndexFileTests { + + @TempDir + File temp; + + @Test + void loadIfPossibleWhenRootIsNotFileReturnsNull() throws IOException { + assertThatIllegalArgumentException() + .isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx")) + .withMessage("URL does not reference a file"); + } + + @Test + void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception { + File root = new File(this.temp, "missing"); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleWhenRootIsFolderThrowsException() throws Exception { + File root = new File(this.temp, "folder"); + root.mkdirs(); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleReturnsInstance() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile).isNotNull(); + } + + @Test + void sizeReturnsNumberOfLines() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.size()).isEqualTo(5); + } + + @Test + void containsFolderWhenFolderIsPresentReturnsTrue() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.containsFolder("BOOT-INF/layers/one/lib")).isTrue(); + assertThat(indexFile.containsFolder("BOOT-INF/layers/one/lib/")).isTrue(); + assertThat(indexFile.containsFolder("BOOT-INF/layers/two/lib")).isTrue(); + } + + @Test + void containsFolderWhenFolderIsMissingReturnsFalse() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.containsFolder("BOOT-INF/layers/nope/lib/")).isFalse(); + } + + @Test + void getUrlsReturnsUrls() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + List urls = indexFile.getUrls(); + List expected = new ArrayList<>(); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/a.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/b.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/c.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/d.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/e.jar")); + assertThat(urls).containsExactly(expected.stream().map(this::toUrl).toArray(URL[]::new)); + } + + private URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException, MalformedURLException { + copyTestIndexFile(); + ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx"); + return indexFile; + } + + private void copyTestIndexFile() throws IOException { + Files.copy(getClass().getResourceAsStream("classpath-index-file.idx"), + new File(this.temp, "test.idx").toPath()); + } + +} 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 2bebbdde0641..c894a3480b29 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 @@ -18,7 +18,9 @@ import java.io.File; import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.junit.jupiter.api.Test; @@ -33,6 +35,7 @@ * Tests for {@link JarLauncher}. * * @author Andy Wilkinson + * @author Madhura Bhave */ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { @@ -67,6 +70,16 @@ void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws } } + @Test + void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + protected final URL[] getExpectedFileUrls(File explodedRoot) { return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx new file mode 100644 index 000000000000..3fad9b2e50ec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx @@ -0,0 +1,5 @@ +BOOT-INF/layers/one/lib/a.jar +BOOT-INF/layers/one/lib/b.jar +BOOT-INF/layers/one/lib/c.jar +BOOT-INF/layers/two/lib/d.jar +BOOT-INF/layers/two/lib/e.jar