Skip to content

Commit

Permalink
Add classpath index support for exploded archives
Browse files Browse the repository at this point in the history
Update the `Repackager` class so that an additional `classpath.idx` file
is written into the jar that provides the original order of the
classpath. The `JarLauncher` class now uses this file when running as
an exploded archive to ensure that the classpath order is the same as
when running from the far jar.

Closes gh-9128

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
  • Loading branch information
mbhave and philwebb committed Jan 16, 2020
1 parent ad72f86 commit 45b1ab4
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -52,6 +56,7 @@
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.0.0
*/
public class JarWriter implements LoaderClassesWriter, AutoCloseable {
Expand Down Expand Up @@ -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<String> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.0.0
*/
public final class Layouts {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Madhura Bhave
* @since 1.0.0
*/
public class Repackager {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()));
}
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -59,6 +63,7 @@
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class RepackagerTests {

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> lines;

private final Set<String> folders;

private ClassPathIndexFile(File root, List<String> 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<URL> 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<String> loadLines(InputStream inputStream) throws IOException {
List<String> 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());
}
}

}
Loading

0 comments on commit 45b1ab4

Please sign in to comment.