Reduce memory consumption of fat/exploded jars
Refactor `spring-boot-loader` to reduce the amount of memory required to load fat & exploded jars. Jar files now no longer store a full list of entry data records, but instead use an array of entry name hashes. Since ClassLoaders often ask each JAR if they contain a particular entry (and mostly they do not), the hash array provides a quick way to deal with misses. Only when a hash does exist is data actually loaded from the underlying file. In addition to the JarFile changes, the Archive abstraction has also been updated to reduce memory consumption. See gh-4882
This commit is contained in:
parent
858a854ce1
commit
e2368b909b
|
@ -24,6 +24,7 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
import org.springframework.boot.loader.archive.Archive;
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
|
@ -66,7 +67,16 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getMainClass() throws Exception {
|
protected String getMainClass() throws Exception {
|
||||||
return this.archive.getMainClass();
|
Manifest manifest = this.archive.getManifest();
|
||||||
|
String mainClass = null;
|
||||||
|
if (manifest != null) {
|
||||||
|
mainClass = manifest.getMainAttributes().getValue("Start-Class");
|
||||||
|
}
|
||||||
|
if (mainClass == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"No 'Start-Class' manifest entry specified in " + this);
|
||||||
|
}
|
||||||
|
return mainClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.springframework.boot.loader;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.boot.loader.archive.Archive;
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
|
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
|
||||||
|
@ -29,7 +28,7 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||||
*/
|
*/
|
||||||
public class JarLauncher extends ExecutableArchiveLauncher {
|
public class JarLauncher extends ExecutableArchiveLauncher {
|
||||||
|
|
||||||
private static final AsciiBytes LIB = new AsciiBytes("lib/");
|
private static final String LIB = "lib/";
|
||||||
|
|
||||||
public JarLauncher() {
|
public JarLauncher() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,20 +204,15 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
||||||
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
|
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object run() throws ClassNotFoundException {
|
public Object run() throws ClassNotFoundException {
|
||||||
String path = name.replace('.', '/').concat(".class");
|
|
||||||
for (URL url : getURLs()) {
|
for (URL url : getURLs()) {
|
||||||
try {
|
try {
|
||||||
if (url.getContent() instanceof JarFile) {
|
if (url.getContent() instanceof JarFile) {
|
||||||
JarFile jarFile = (JarFile) url.getContent();
|
JarFile jarFile = (JarFile) url.getContent();
|
||||||
// Check the jar entry data before needlessly creating the
|
if (jarFile.getManifest() != null) {
|
||||||
// manifest
|
|
||||||
if (jarFile.getJarEntryData(path) != null
|
|
||||||
&& jarFile.getManifest() != null) {
|
|
||||||
definePackage(packageName, jarFile.getManifest(),
|
definePackage(packageName, jarFile.getManifest(),
|
||||||
url);
|
url);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
|
|
|
@ -21,14 +21,15 @@ import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
@ -39,9 +40,7 @@ import org.springframework.boot.loader.archive.Archive;
|
||||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||||
import org.springframework.boot.loader.archive.ExplodedArchive;
|
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||||
import org.springframework.boot.loader.archive.FilteredArchive;
|
|
||||||
import org.springframework.boot.loader.archive.JarFileArchive;
|
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
import org.springframework.boot.loader.util.SystemPropertyUtils;
|
import org.springframework.boot.loader.util.SystemPropertyUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,8 +121,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
*/
|
*/
|
||||||
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
|
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
|
||||||
|
|
||||||
private static final List<String> DEFAULT_PATHS = Arrays.asList();
|
|
||||||
|
|
||||||
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
|
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
|
||||||
|
|
||||||
private static final URL[] EMPTY_URLS = {};
|
private static final URL[] EMPTY_URLS = {};
|
||||||
|
@ -132,7 +129,7 @@ public class PropertiesLauncher extends Launcher {
|
||||||
|
|
||||||
private final JavaAgentDetector javaAgentDetector;
|
private final JavaAgentDetector javaAgentDetector;
|
||||||
|
|
||||||
private List<String> paths = new ArrayList<String>(DEFAULT_PATHS);
|
private List<String> paths = new ArrayList<String>();
|
||||||
|
|
||||||
private final Properties properties = new Properties();
|
private final Properties properties = new Properties();
|
||||||
|
|
||||||
|
@ -168,7 +165,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
config = SystemPropertyUtils.resolvePlaceholders(
|
config = SystemPropertyUtils.resolvePlaceholders(
|
||||||
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config));
|
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config));
|
||||||
InputStream resource = getResource(config);
|
InputStream resource = getResource(config);
|
||||||
|
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
log("Found: " + config);
|
log("Found: " + config);
|
||||||
try {
|
try {
|
||||||
|
@ -353,7 +349,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
|
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
|
||||||
String loaderClassName) throws Exception {
|
String loaderClassName) throws Exception {
|
||||||
|
|
||||||
Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
|
Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
|
||||||
.forName(loaderClassName, true, parent);
|
.forName(loaderClassName, true, parent);
|
||||||
|
|
||||||
|
@ -363,7 +358,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
catch (NoSuchMethodException ex) {
|
catch (NoSuchMethodException ex) {
|
||||||
// Ignore and try with URLs
|
// Ignore and try with URLs
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return loaderClass.getConstructor(URL[].class, ClassLoader.class)
|
return loaderClass.getConstructor(URL[].class, ClassLoader.class)
|
||||||
.newInstance(new URL[0], parent);
|
.newInstance(new URL[0], parent);
|
||||||
|
@ -371,7 +365,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
catch (NoSuchMethodException ex) {
|
catch (NoSuchMethodException ex) {
|
||||||
// Ignore and try without any arguments
|
// Ignore and try without any arguments
|
||||||
}
|
}
|
||||||
|
|
||||||
return loaderClass.newInstance();
|
return loaderClass.newInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,21 +377,18 @@ public class PropertiesLauncher extends Launcher {
|
||||||
manifestKey = propertyKey.replace(".", "-");
|
manifestKey = propertyKey.replace(".", "-");
|
||||||
manifestKey = toCamelCase(manifestKey);
|
manifestKey = toCamelCase(manifestKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
String property = SystemPropertyUtils.getProperty(propertyKey);
|
String property = SystemPropertyUtils.getProperty(propertyKey);
|
||||||
if (property != null) {
|
if (property != null) {
|
||||||
String value = SystemPropertyUtils.resolvePlaceholders(property);
|
String value = SystemPropertyUtils.resolvePlaceholders(property);
|
||||||
log("Property '" + propertyKey + "' from environment: " + value);
|
log("Property '" + propertyKey + "' from environment: " + value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.properties.containsKey(propertyKey)) {
|
if (this.properties.containsKey(propertyKey)) {
|
||||||
String value = SystemPropertyUtils
|
String value = SystemPropertyUtils
|
||||||
.resolvePlaceholders(this.properties.getProperty(propertyKey));
|
.resolvePlaceholders(this.properties.getProperty(propertyKey));
|
||||||
log("Property '" + propertyKey + "' from properties: " + value);
|
log("Property '" + propertyKey + "' from properties: " + value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prefer home dir for MANIFEST if there is one
|
// Prefer home dir for MANIFEST if there is one
|
||||||
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
|
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
|
||||||
|
@ -412,7 +402,6 @@ public class PropertiesLauncher extends Launcher {
|
||||||
catch (IllegalStateException ex) {
|
catch (IllegalStateException ex) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise try the parent archive
|
// Otherwise try the parent archive
|
||||||
Manifest manifest = createArchive().getManifest();
|
Manifest manifest = createArchive().getManifest();
|
||||||
if (manifest != null) {
|
if (manifest != null) {
|
||||||
|
@ -478,7 +467,7 @@ public class PropertiesLauncher extends Launcher {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Archive getNestedArchive(final String root) throws Exception {
|
private Archive getNestedArchive(String root) throws Exception {
|
||||||
if (root.startsWith("/")
|
if (root.startsWith("/")
|
||||||
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
|
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
|
||||||
// If home dir is same as parent archive, no need to add it twice.
|
// If home dir is same as parent archive, no need to add it twice.
|
||||||
|
@ -628,40 +617,84 @@ public class PropertiesLauncher extends Launcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience class for finding nested archives (archive entries that can be
|
|
||||||
* classpath entries).
|
|
||||||
*/
|
|
||||||
private static final class ArchiveEntryFilter implements EntryFilter {
|
|
||||||
|
|
||||||
private static final AsciiBytes DOT_JAR = new AsciiBytes(".jar");
|
|
||||||
|
|
||||||
private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(Entry entry) {
|
|
||||||
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience class for finding nested archives that have a prefix in their file path
|
* Convenience class for finding nested archives that have a prefix in their file path
|
||||||
* (e.g. "lib/").
|
* (e.g. "lib/").
|
||||||
*/
|
*/
|
||||||
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
|
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
|
||||||
|
|
||||||
private final AsciiBytes prefix;
|
private final String prefix;
|
||||||
|
|
||||||
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
|
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
|
||||||
|
|
||||||
private PrefixMatchingArchiveFilter(String prefix) {
|
private PrefixMatchingArchiveFilter(String prefix) {
|
||||||
this.prefix = new AsciiBytes(prefix);
|
this.prefix = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(Entry entry) {
|
public boolean matches(Entry entry) {
|
||||||
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
|
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience class for finding nested archives (archive entries that can be
|
||||||
|
* classpath entries).
|
||||||
|
*/
|
||||||
|
private static final class ArchiveEntryFilter implements EntryFilter {
|
||||||
|
|
||||||
|
private static final String DOT_JAR = ".jar";
|
||||||
|
|
||||||
|
private static final String DOT_ZIP = ".zip";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Entry entry) {
|
||||||
|
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorator to apply an {@link Archive.EntryFilter} to an existing {@link Archive}.
|
||||||
|
*/
|
||||||
|
private static class FilteredArchive implements Archive {
|
||||||
|
|
||||||
|
private final Archive parent;
|
||||||
|
|
||||||
|
private final EntryFilter filter;
|
||||||
|
|
||||||
|
FilteredArchive(Archive parent, EntryFilter filter) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
return this.parent.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
return this.parent.getManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Entry> iterator() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Archive> getNestedArchives(final EntryFilter filter)
|
||||||
|
throws IOException {
|
||||||
|
return this.parent.getNestedArchives(new EntryFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean matches(Entry entry) {
|
||||||
|
return FilteredArchive.this.filter.matches(entry)
|
||||||
|
&& filter.matches(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.springframework.boot.loader;
|
package org.springframework.boot.loader;
|
||||||
|
|
||||||
import org.springframework.boot.loader.archive.Archive;
|
import org.springframework.boot.loader.archive.Archive;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
|
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
|
||||||
|
@ -29,14 +28,13 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||||
*/
|
*/
|
||||||
public class WarLauncher extends ExecutableArchiveLauncher {
|
public class WarLauncher extends ExecutableArchiveLauncher {
|
||||||
|
|
||||||
private static final AsciiBytes WEB_INF = new AsciiBytes("WEB-INF/");
|
private static final String WEB_INF = "WEB-INF/";
|
||||||
|
|
||||||
private static final AsciiBytes WEB_INF_CLASSES = WEB_INF.append("classes/");
|
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
|
||||||
|
|
||||||
private static final AsciiBytes WEB_INF_LIB = WEB_INF.append("lib/");
|
private static final String WEB_INF_LIB = WEB_INF + "lib/";
|
||||||
|
|
||||||
private static final AsciiBytes WEB_INF_LIB_PROVIDED = WEB_INF
|
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
|
||||||
.append("lib-provided/");
|
|
||||||
|
|
||||||
public WarLauncher() {
|
public WarLauncher() {
|
||||||
super();
|
super();
|
||||||
|
|
|
@ -19,12 +19,10 @@ package org.springframework.boot.loader.archive;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
import org.springframework.boot.loader.Launcher;
|
import org.springframework.boot.loader.Launcher;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An archive that can be launched by the {@link Launcher}.
|
* An archive that can be launched by the {@link Launcher}.
|
||||||
|
@ -32,56 +30,21 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @see JarFileArchive
|
* @see JarFileArchive
|
||||||
*/
|
*/
|
||||||
public abstract class Archive {
|
public interface Archive extends Iterable<Archive.Entry> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a URL that can be used to load the archive.
|
* Returns a URL that can be used to load the archive.
|
||||||
* @return the archive URL
|
* @return the archive URL
|
||||||
* @throws MalformedURLException if the URL is malformed
|
* @throws MalformedURLException if the URL is malformed
|
||||||
*/
|
*/
|
||||||
public abstract URL getUrl() throws MalformedURLException;
|
URL getUrl() throws MalformedURLException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain the main class that should be used to launch the application. By default
|
|
||||||
* this method uses a {@code Start-Class} manifest entry.
|
|
||||||
* @return the main class
|
|
||||||
* @throws Exception if the main class cannot be obtained
|
|
||||||
*/
|
|
||||||
public String getMainClass() throws Exception {
|
|
||||||
Manifest manifest = getManifest();
|
|
||||||
String mainClass = null;
|
|
||||||
if (manifest != null) {
|
|
||||||
mainClass = manifest.getMainAttributes().getValue("Start-Class");
|
|
||||||
}
|
|
||||||
if (mainClass == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"No 'Start-Class' manifest entry specified in " + this);
|
|
||||||
}
|
|
||||||
return mainClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
try {
|
|
||||||
return getUrl().toString();
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
return "archive";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the manifest of the archive.
|
* Returns the manifest of the archive.
|
||||||
* @return the manifest
|
* @return the manifest
|
||||||
* @throws IOException if the manifest cannot be read
|
* @throws IOException if the manifest cannot be read
|
||||||
*/
|
*/
|
||||||
public abstract Manifest getManifest() throws IOException;
|
Manifest getManifest() throws IOException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all entries from the archive.
|
|
||||||
* @return the archive entries
|
|
||||||
*/
|
|
||||||
public abstract Collection<Entry> getEntries();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns nested {@link Archive}s for entries that match the specified filter.
|
* Returns nested {@link Archive}s for entries that match the specified filter.
|
||||||
|
@ -89,22 +52,12 @@ public abstract class Archive {
|
||||||
* @return nested archives
|
* @return nested archives
|
||||||
* @throws IOException if nested archives cannot be read
|
* @throws IOException if nested archives cannot be read
|
||||||
*/
|
*/
|
||||||
public abstract List<Archive> getNestedArchives(EntryFilter filter)
|
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a filtered version of the archive.
|
|
||||||
* @param filter the filter to apply
|
|
||||||
* @return a filter archive
|
|
||||||
* @throws IOException if the archive cannot be read
|
|
||||||
*/
|
|
||||||
public abstract Archive getFilteredArchive(EntryRenameFilter filter)
|
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single entry in the archive.
|
* Represents a single entry in the archive.
|
||||||
*/
|
*/
|
||||||
public interface Entry {
|
interface Entry {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@code true} if the entry represents a directory.
|
* Returns {@code true} if the entry represents a directory.
|
||||||
|
@ -116,14 +69,14 @@ public abstract class Archive {
|
||||||
* Returns the name of the entry.
|
* Returns the name of the entry.
|
||||||
* @return the name of the entry
|
* @return the name of the entry
|
||||||
*/
|
*/
|
||||||
AsciiBytes getName();
|
String getName();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strategy interface to filter {@link Entry Entries}.
|
* Strategy interface to filter {@link Entry Entries}.
|
||||||
*/
|
*/
|
||||||
public interface EntryFilter {
|
interface EntryFilter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the jar entry filter.
|
* Apply the jar entry filter.
|
||||||
|
@ -134,21 +87,4 @@ public abstract class Archive {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Strategy interface to filter or rename {@link Entry Entries}.
|
|
||||||
*/
|
|
||||||
public interface EntryRenameFilter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the jar entry filter.
|
|
||||||
* @param entryName the current entry name. This may be different that the
|
|
||||||
* original entry name if a previous filter has been applied
|
|
||||||
* @param entry the entry to filter
|
|
||||||
* @return the new name of the entry or {@code null} if the entry should not be
|
|
||||||
* included.
|
|
||||||
*/
|
|
||||||
AsciiBytes apply(AsciiBytes entryName, Entry entry);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,46 +18,39 @@ package org.springframework.boot.loader.archive;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Archive} implementation backed by an exploded archive directory.
|
* {@link Archive} implementation backed by an exploded archive directory.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public class ExplodedArchive extends Archive {
|
public class ExplodedArchive implements Archive {
|
||||||
|
|
||||||
private static final Set<String> SKIPPED_NAMES = new HashSet<String>(
|
private static final Set<String> SKIPPED_NAMES = new HashSet<String>(
|
||||||
Arrays.asList(".", ".."));
|
Arrays.asList(".", ".."));
|
||||||
|
|
||||||
private static final AsciiBytes MANIFEST_ENTRY_NAME = new AsciiBytes(
|
|
||||||
"META-INF/MANIFEST.MF");
|
|
||||||
|
|
||||||
private final File root;
|
private final File root;
|
||||||
|
|
||||||
private Map<AsciiBytes, Entry> entries = new LinkedHashMap<AsciiBytes, Entry>();
|
private final boolean recursive;
|
||||||
|
|
||||||
|
private File manifestFile;
|
||||||
|
|
||||||
private Manifest manifest;
|
private Manifest manifest;
|
||||||
|
|
||||||
private boolean filtered = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link ExplodedArchive} instance.
|
* Create a new {@link ExplodedArchive} instance.
|
||||||
* @param root the root folder
|
* @param root the root folder
|
||||||
|
@ -78,52 +71,24 @@ public class ExplodedArchive extends Archive {
|
||||||
throw new IllegalArgumentException("Invalid source folder " + root);
|
throw new IllegalArgumentException("Invalid source folder " + root);
|
||||||
}
|
}
|
||||||
this.root = root;
|
this.root = root;
|
||||||
buildEntries(root, recursive);
|
this.recursive = recursive;
|
||||||
this.entries = Collections.unmodifiableMap(this.entries);
|
this.manifestFile = getManifestFile(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExplodedArchive(File root, Map<AsciiBytes, Entry> entries) {
|
private File getManifestFile(File root) {
|
||||||
this.root = root;
|
File metaInf = new File(root, "META-INF");
|
||||||
// The entries are pre-filtered
|
return new File(metaInf, "MANIFEST.MF");
|
||||||
this.filtered = true;
|
|
||||||
this.entries = Collections.unmodifiableMap(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildEntries(File file, boolean recursive) {
|
|
||||||
if (!file.equals(this.root)) {
|
|
||||||
String name = file.toURI().getPath()
|
|
||||||
.substring(this.root.toURI().getPath().length());
|
|
||||||
FileEntry entry = new FileEntry(new AsciiBytes(name), file);
|
|
||||||
this.entries.put(entry.getName(), entry);
|
|
||||||
}
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
File[] files = file.listFiles();
|
|
||||||
if (files == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (File child : files) {
|
|
||||||
if (!SKIPPED_NAMES.contains(child.getName())) {
|
|
||||||
if (file.equals(this.root) || recursive
|
|
||||||
|| file.getName().equals("META-INF")) {
|
|
||||||
buildEntries(child, recursive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getUrl() throws MalformedURLException {
|
public URL getUrl() throws MalformedURLException {
|
||||||
FilteredURLStreamHandler handler = this.filtered ? new FilteredURLStreamHandler()
|
return new URL("file", "", -1, this.root.toURI().getPath());
|
||||||
: null;
|
|
||||||
return new URL("file", "", -1, this.root.toURI().getPath(), handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Manifest getManifest() throws IOException {
|
public Manifest getManifest() throws IOException {
|
||||||
if (this.manifest == null && this.entries.containsKey(MANIFEST_ENTRY_NAME)) {
|
if (this.manifest == null && this.manifestFile.exists()) {
|
||||||
FileEntry entry = (FileEntry) this.entries.get(MANIFEST_ENTRY_NAME);
|
FileInputStream inputStream = new FileInputStream(this.manifestFile);
|
||||||
FileInputStream inputStream = new FileInputStream(entry.getFile());
|
|
||||||
try {
|
try {
|
||||||
this.manifest = new Manifest(inputStream);
|
this.manifest = new Manifest(inputStream);
|
||||||
}
|
}
|
||||||
|
@ -137,7 +102,7 @@ public class ExplodedArchive extends Archive {
|
||||||
@Override
|
@Override
|
||||||
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
||||||
List<Archive> nestedArchives = new ArrayList<Archive>();
|
List<Archive> nestedArchives = new ArrayList<Archive>();
|
||||||
for (Entry entry : getEntries()) {
|
for (Entry entry : this) {
|
||||||
if (filter.matches(entry)) {
|
if (filter.matches(entry)) {
|
||||||
nestedArchives.add(getNestedArchive(entry));
|
nestedArchives.add(getNestedArchive(entry));
|
||||||
}
|
}
|
||||||
|
@ -146,8 +111,8 @@ public class ExplodedArchive extends Archive {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Entry> getEntries() {
|
public Iterator<Entry> iterator() {
|
||||||
return Collections.unmodifiableCollection(this.entries.values());
|
return new FileEntryIterator(this.root, this.recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||||
|
@ -157,25 +122,94 @@ public class ExplodedArchive extends Archive {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException {
|
public String toString() {
|
||||||
Map<AsciiBytes, Entry> filteredEntries = new LinkedHashMap<AsciiBytes, Archive.Entry>();
|
try {
|
||||||
for (Map.Entry<AsciiBytes, Entry> entry : this.entries.entrySet()) {
|
return getUrl().toString();
|
||||||
AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue());
|
|
||||||
if (filteredName != null) {
|
|
||||||
filteredEntries.put(filteredName, new FileEntry(filteredName,
|
|
||||||
((FileEntry) entry.getValue()).getFile()));
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return "exploded archive";
|
||||||
}
|
}
|
||||||
return new ExplodedArchive(this.root, filteredEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FileEntry implements Entry {
|
/**
|
||||||
|
* File based {@link Entry} {@link Iterator}.
|
||||||
|
*/
|
||||||
|
private static class FileEntryIterator implements Iterator<Entry> {
|
||||||
|
|
||||||
private final AsciiBytes name;
|
private final File root;
|
||||||
|
|
||||||
|
private final boolean recursive;
|
||||||
|
|
||||||
|
private final Deque<Iterator<File>> stack = new LinkedList<Iterator<File>>();
|
||||||
|
|
||||||
|
private File current;
|
||||||
|
|
||||||
|
FileEntryIterator(File root, boolean recursive) {
|
||||||
|
this.root = root;
|
||||||
|
this.recursive = recursive;
|
||||||
|
this.stack.add(listFiles(root));
|
||||||
|
this.current = poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.current != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entry next() {
|
||||||
|
if (this.current == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
File file = this.current;
|
||||||
|
if (file.isDirectory()
|
||||||
|
&& (this.recursive || file.getParentFile().equals(this.root))) {
|
||||||
|
this.stack.addFirst(listFiles(file));
|
||||||
|
}
|
||||||
|
this.current = poll();
|
||||||
|
String name = file.toURI().getPath()
|
||||||
|
.substring(this.root.toURI().getPath().length());
|
||||||
|
return new FileEntry(name, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Iterator<File> listFiles(File file) {
|
||||||
|
File[] files = file.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return Collections.<File>emptyList().iterator();
|
||||||
|
}
|
||||||
|
return Arrays.asList(files).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File poll() {
|
||||||
|
while (!this.stack.isEmpty()) {
|
||||||
|
while (this.stack.peek().hasNext()) {
|
||||||
|
File file = this.stack.peek().next();
|
||||||
|
if (!SKIPPED_NAMES.contains(file.getName())) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stack.poll();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("remove");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Entry} backed by a File.
|
||||||
|
*/
|
||||||
|
private static class FileEntry implements Entry {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
|
|
||||||
FileEntry(AsciiBytes name, File file) {
|
FileEntry(String name, File file) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
@ -190,43 +224,9 @@ public class ExplodedArchive extends Archive {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AsciiBytes getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link URLStreamHandler} that respects filtered entries.
|
|
||||||
*/
|
|
||||||
private class FilteredURLStreamHandler extends URLStreamHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected URLConnection openConnection(URL url) throws IOException {
|
|
||||||
String name = url.getPath()
|
|
||||||
.substring(ExplodedArchive.this.root.toURI().getPath().length());
|
|
||||||
if (ExplodedArchive.this.entries.containsKey(new AsciiBytes(name))) {
|
|
||||||
return new URL(url.toString()).openConnection();
|
|
||||||
}
|
|
||||||
return new FileNotFoundURLConnection(url, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link URLConnection} used to represent a filtered file.
|
|
||||||
*/
|
|
||||||
private static class FileNotFoundURLConnection extends URLConnection {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
FileNotFoundURLConnection(URL url, String name) {
|
|
||||||
super(url);
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect() throws IOException {
|
|
||||||
throw new FileNotFoundException(this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2014 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
|
|
||||||
*
|
|
||||||
* http://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.archive;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
|
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decorator to apply an {@link Archive.EntryFilter} to an existing {@link Archive}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
*/
|
|
||||||
public class FilteredArchive extends Archive {
|
|
||||||
|
|
||||||
private final Archive parent;
|
|
||||||
|
|
||||||
private final EntryFilter filter;
|
|
||||||
|
|
||||||
public FilteredArchive(Archive parent, EntryFilter filter) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public URL getUrl() throws MalformedURLException {
|
|
||||||
return this.parent.getUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMainClass() throws Exception {
|
|
||||||
return this.parent.getMainClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Manifest getManifest() throws IOException {
|
|
||||||
return this.parent.getManifest();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<Entry> getEntries() {
|
|
||||||
List<Entry> nested = new ArrayList<Entry>();
|
|
||||||
for (Entry entry : this.parent.getEntries()) {
|
|
||||||
if (this.filter.matches(entry)) {
|
|
||||||
nested.add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Collections.unmodifiableList(nested);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Archive> getNestedArchives(final EntryFilter filter) throws IOException {
|
|
||||||
return this.parent.getNestedArchives(new EntryFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean matches(Entry entry) {
|
|
||||||
return FilteredArchive.this.filter.matches(entry)
|
|
||||||
&& filter.matches(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
|
|
||||||
return this.parent.getFilteredArchive(new EntryRenameFilter() {
|
|
||||||
@Override
|
|
||||||
public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
|
|
||||||
return FilteredArchive.this.filter.matches(entry)
|
|
||||||
? filter.apply(entryName, entry) : null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -24,18 +24,16 @@ import java.io.OutputStream;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
import org.springframework.boot.loader.jar.JarEntryData;
|
|
||||||
import org.springframework.boot.loader.jar.JarEntryFilter;
|
|
||||||
import org.springframework.boot.loader.jar.JarFile;
|
import org.springframework.boot.loader.jar.JarFile;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Archive} implementation backed by a {@link JarFile}.
|
* {@link Archive} implementation backed by a {@link JarFile}.
|
||||||
|
@ -43,16 +41,14 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
public class JarFileArchive extends Archive {
|
public class JarFileArchive implements Archive {
|
||||||
|
|
||||||
private static final AsciiBytes UNPACK_MARKER = new AsciiBytes("UNPACK:");
|
private static final String UNPACK_MARKER = "UNPACK:";
|
||||||
|
|
||||||
private static final int BUFFER_SIZE = 32 * 1024;
|
private static final int BUFFER_SIZE = 32 * 1024;
|
||||||
|
|
||||||
private final JarFile jarFile;
|
private final JarFile jarFile;
|
||||||
|
|
||||||
private final List<Entry> entries;
|
|
||||||
|
|
||||||
private URL url;
|
private URL url;
|
||||||
|
|
||||||
private File tempUnpackFolder;
|
private File tempUnpackFolder;
|
||||||
|
@ -68,11 +64,6 @@ public class JarFileArchive extends Archive {
|
||||||
|
|
||||||
public JarFileArchive(JarFile jarFile) {
|
public JarFileArchive(JarFile jarFile) {
|
||||||
this.jarFile = jarFile;
|
this.jarFile = jarFile;
|
||||||
ArrayList<Entry> jarFileEntries = new ArrayList<Entry>();
|
|
||||||
for (JarEntryData data : jarFile) {
|
|
||||||
jarFileEntries.add(new JarFileEntry(data));
|
|
||||||
}
|
|
||||||
this.entries = Collections.unmodifiableList(jarFileEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -91,7 +82,7 @@ public class JarFileArchive extends Archive {
|
||||||
@Override
|
@Override
|
||||||
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
||||||
List<Archive> nestedArchives = new ArrayList<Archive>();
|
List<Archive> nestedArchives = new ArrayList<Archive>();
|
||||||
for (Entry entry : getEntries()) {
|
for (Entry entry : this) {
|
||||||
if (filter.matches(entry)) {
|
if (filter.matches(entry)) {
|
||||||
nestedArchives.add(getNestedArchive(entry));
|
nestedArchives.add(getNestedArchive(entry));
|
||||||
}
|
}
|
||||||
|
@ -100,27 +91,27 @@ public class JarFileArchive extends Archive {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Entry> getEntries() {
|
public Iterator<Entry> iterator() {
|
||||||
return Collections.unmodifiableCollection(this.entries);
|
return new EntryIterator(this.jarFile.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||||
JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
|
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
|
||||||
if (data.getComment().startsWith(UNPACK_MARKER)) {
|
if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
|
||||||
return getUnpackedNestedArchive(data);
|
return getUnpackedNestedArchive(jarEntry);
|
||||||
}
|
}
|
||||||
JarFile jarFile = this.jarFile.getNestedJarFile(data);
|
JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
|
||||||
return new JarFileArchive(jarFile);
|
return new JarFileArchive(jarFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Archive getUnpackedNestedArchive(JarEntryData data) throws IOException {
|
private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
|
||||||
String name = data.getName().toString();
|
String name = jarEntry.getName();
|
||||||
if (name.lastIndexOf("/") != -1) {
|
if (name.lastIndexOf("/") != -1) {
|
||||||
name = name.substring(name.lastIndexOf("/") + 1);
|
name = name.substring(name.lastIndexOf("/") + 1);
|
||||||
}
|
}
|
||||||
File file = new File(getTempUnpackFolder(), name);
|
File file = new File(getTempUnpackFolder(), name);
|
||||||
if (!file.exists() || file.length() != data.getSize()) {
|
if (!file.exists() || file.length() != jarEntry.getSize()) {
|
||||||
unpack(data, file);
|
unpack(jarEntry, file);
|
||||||
}
|
}
|
||||||
return new JarFileArchive(file, file.toURI().toURL());
|
return new JarFileArchive(file, file.toURI().toURL());
|
||||||
}
|
}
|
||||||
|
@ -147,8 +138,8 @@ public class JarFileArchive extends Archive {
|
||||||
"Failed to create unpack folder in directory '" + parent + "'");
|
"Failed to create unpack folder in directory '" + parent + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unpack(JarEntryData data, File file) throws IOException {
|
private void unpack(JarEntry entry, File file) throws IOException {
|
||||||
InputStream inputStream = data.getData().getInputStream(ResourceAccess.ONCE);
|
InputStream inputStream = this.jarFile.getInputStream(entry, ResourceAccess.ONCE);
|
||||||
try {
|
try {
|
||||||
OutputStream outputStream = new FileOutputStream(file);
|
OutputStream outputStream = new FileOutputStream(file);
|
||||||
try {
|
try {
|
||||||
|
@ -169,14 +160,41 @@ public class JarFileArchive extends Archive {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
|
public String toString() {
|
||||||
JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
|
try {
|
||||||
@Override
|
return getUrl().toString();
|
||||||
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
|
||||||
return filter.apply(name, new JarFileEntry(entryData));
|
|
||||||
}
|
}
|
||||||
});
|
catch (Exception ex) {
|
||||||
return new JarFileArchive(filteredJar);
|
return "jar archive";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
|
||||||
|
*/
|
||||||
|
private static class EntryIterator implements Iterator<Entry> {
|
||||||
|
|
||||||
|
private final Enumeration<JarEntry> enumeration;
|
||||||
|
|
||||||
|
EntryIterator(Enumeration<JarEntry> enumeration) {
|
||||||
|
this.enumeration = enumeration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.enumeration.hasMoreElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entry next() {
|
||||||
|
return new JarFileEntry(this.enumeration.nextElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("remove");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,24 +202,24 @@ public class JarFileArchive extends Archive {
|
||||||
*/
|
*/
|
||||||
private static class JarFileEntry implements Entry {
|
private static class JarFileEntry implements Entry {
|
||||||
|
|
||||||
private final JarEntryData entryData;
|
private final JarEntry jarEntry;
|
||||||
|
|
||||||
JarFileEntry(JarEntryData entryData) {
|
JarFileEntry(JarEntry jarEntry) {
|
||||||
this.entryData = entryData;
|
this.jarEntry = jarEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JarEntryData getJarEntryData() {
|
public JarEntry getJarEntry() {
|
||||||
return this.entryData;
|
return this.jarEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isDirectory() {
|
public boolean isDirectory() {
|
||||||
return this.entryData.isDirectory();
|
return this.jarEntry.isDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AsciiBytes getName() {
|
public String getName() {
|
||||||
return this.entryData.getName();
|
return this.jarEntry.getName().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.loader.util;
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
@ -24,14 +24,10 @@ import java.nio.charset.Charset;
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public final class AsciiBytes {
|
final class AsciiBytes {
|
||||||
|
|
||||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||||
|
|
||||||
private static final int INITIAL_HASH = 7;
|
|
||||||
|
|
||||||
private static final int MULTIPLIER = 31;
|
|
||||||
|
|
||||||
private final byte[] bytes;
|
private final byte[] bytes;
|
||||||
|
|
||||||
private final int offset;
|
private final int offset;
|
||||||
|
@ -40,11 +36,13 @@ public final class AsciiBytes {
|
||||||
|
|
||||||
private String string;
|
private String string;
|
||||||
|
|
||||||
|
private int hash;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link AsciiBytes} from the specified String.
|
* Create a new {@link AsciiBytes} from the specified String.
|
||||||
* @param string the source string
|
* @param string the source string
|
||||||
*/
|
*/
|
||||||
public AsciiBytes(String string) {
|
AsciiBytes(String string) {
|
||||||
this(string.getBytes(UTF_8));
|
this(string.getBytes(UTF_8));
|
||||||
this.string = string;
|
this.string = string;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +52,7 @@ public final class AsciiBytes {
|
||||||
* are not expected to change.
|
* are not expected to change.
|
||||||
* @param bytes the source bytes
|
* @param bytes the source bytes
|
||||||
*/
|
*/
|
||||||
public AsciiBytes(byte[] bytes) {
|
AsciiBytes(byte[] bytes) {
|
||||||
this(bytes, 0, bytes.length);
|
this(bytes, 0, bytes.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +63,7 @@ public final class AsciiBytes {
|
||||||
* @param offset the offset
|
* @param offset the offset
|
||||||
* @param length the length
|
* @param length the length
|
||||||
*/
|
*/
|
||||||
public AsciiBytes(byte[] bytes, int offset, int length) {
|
AsciiBytes(byte[] bytes, int offset, int length) {
|
||||||
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
|
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
|
||||||
throw new IndexOutOfBoundsException();
|
throw new IndexOutOfBoundsException();
|
||||||
}
|
}
|
||||||
|
@ -155,12 +153,28 @@ public final class AsciiBytes {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = INITIAL_HASH;
|
int hash = this.hash;
|
||||||
for (int i = 0; i < this.length; i++) {
|
if (hash == 0 && this.bytes.length > 0) {
|
||||||
hash = MULTIPLIER * hash + this.bytes[this.offset + i];
|
for (int i = this.offset; i < this.offset + this.length; i++) {
|
||||||
|
int b = this.bytes[i] & 0xff;
|
||||||
|
if (b > 0x7F) {
|
||||||
|
// Decode multi-byte UTF
|
||||||
|
for (int size = 0; size < 3; size++) {
|
||||||
|
if ((b & (0x40 >> size)) == 0) {
|
||||||
|
b = b & (0x1F >> size);
|
||||||
|
for (int j = 0; j < size; j++) {
|
||||||
|
b <<= 6;
|
||||||
|
b |= this.bytes[++i] & 0x3F;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash = 31 * hash + b;
|
||||||
|
}
|
||||||
|
this.hash = hash;
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -185,4 +199,17 @@ public final class AsciiBytes {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int hashCode(String string) {
|
||||||
|
// We're compatible with String's hashcode
|
||||||
|
return string.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int hashCode(int hash, String string) {
|
||||||
|
char[] chars = string.toCharArray();
|
||||||
|
for (int i = 0; i < chars.length; i++) {
|
||||||
|
hash = 31 * hash + chars[i];
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -18,30 +18,24 @@ package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
|
|
||||||
import org.springframework.boot.loader.data.RandomAccessData;
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the underlying data of a {@link JarEntry}, allowing creation to be deferred until
|
* A ZIP File "Central directory file header record" (CDFH).
|
||||||
* the entry is actually needed.
|
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @see <a href="http://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
|
||||||
*/
|
*/
|
||||||
public final class JarEntryData {
|
|
||||||
|
|
||||||
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
final class CentralDirectoryFileHeader implements FileHeader {
|
||||||
|
|
||||||
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
||||||
|
|
||||||
private final JarFile source;
|
|
||||||
|
|
||||||
private final byte[] header;
|
private final byte[] header;
|
||||||
|
|
||||||
private AsciiBytes name;
|
private AsciiBytes name;
|
||||||
|
@ -52,15 +46,8 @@ public final class JarEntryData {
|
||||||
|
|
||||||
private final long localHeaderOffset;
|
private final long localHeaderOffset;
|
||||||
|
|
||||||
private RandomAccessData data;
|
CentralDirectoryFileHeader(byte[] header, InputStream inputStream)
|
||||||
|
|
||||||
private SoftReference<JarEntry> entry;
|
|
||||||
|
|
||||||
JarFile nestedJar;
|
|
||||||
|
|
||||||
public JarEntryData(JarFile source, byte[] header, InputStream inputStream)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
this.source = source;
|
|
||||||
this.header = header;
|
this.header = header;
|
||||||
long nameLength = Bytes.littleEndianValue(header, 28, 2);
|
long nameLength = Bytes.littleEndianValue(header, 28, 2);
|
||||||
long extraLength = Bytes.littleEndianValue(header, 30, 2);
|
long extraLength = Bytes.littleEndianValue(header, 30, 2);
|
||||||
|
@ -71,79 +58,20 @@ public final class JarEntryData {
|
||||||
this.localHeaderOffset = Bytes.littleEndianValue(header, 42, 4);
|
this.localHeaderOffset = Bytes.littleEndianValue(header, 42, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JarEntryData(JarEntryData master, JarFile source, AsciiBytes name) {
|
|
||||||
this.header = master.header;
|
|
||||||
this.extra = master.extra;
|
|
||||||
this.comment = master.comment;
|
|
||||||
this.localHeaderOffset = master.localHeaderOffset;
|
|
||||||
this.source = source;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setName(AsciiBytes name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
JarFile getSource() {
|
|
||||||
return this.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream getInputStream() throws IOException {
|
|
||||||
InputStream inputStream = getData().getInputStream(ResourceAccess.PER_READ);
|
|
||||||
if (getMethod() == ZipEntry.DEFLATED) {
|
|
||||||
inputStream = new ZipInflaterInputStream(inputStream, getSize());
|
|
||||||
}
|
|
||||||
return inputStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the underlying {@link RandomAccessData} for this entry. Generally this
|
|
||||||
* method should not be called directly and instead data should be accessed via
|
|
||||||
* {@link JarFile#getInputStream(ZipEntry)}.
|
|
||||||
* @return the data
|
|
||||||
* @throws IOException if the data cannot be read
|
|
||||||
*/
|
|
||||||
public RandomAccessData getData() throws IOException {
|
|
||||||
if (this.data == null) {
|
|
||||||
// aspectjrt-1.7.4.jar has a different ext bytes length in the
|
|
||||||
// local directory to the central directory. We need to re-read
|
|
||||||
// here to skip them
|
|
||||||
byte[] localHeader = Bytes.get(this.source.getData()
|
|
||||||
.getSubsection(this.localHeaderOffset, LOCAL_FILE_HEADER_SIZE));
|
|
||||||
long nameLength = Bytes.littleEndianValue(localHeader, 26, 2);
|
|
||||||
long extraLength = Bytes.littleEndianValue(localHeader, 28, 2);
|
|
||||||
this.data = this.source.getData().getSubsection(this.localHeaderOffset
|
|
||||||
+ LOCAL_FILE_HEADER_SIZE + nameLength + extraLength,
|
|
||||||
getCompressedSize());
|
|
||||||
}
|
|
||||||
return this.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
JarEntry asJarEntry() {
|
|
||||||
JarEntry entry = (this.entry == null ? null : this.entry.get());
|
|
||||||
if (entry == null) {
|
|
||||||
entry = new JarEntry(this);
|
|
||||||
entry.setCompressedSize(getCompressedSize());
|
|
||||||
entry.setMethod(getMethod());
|
|
||||||
entry.setCrc(getCrc());
|
|
||||||
entry.setSize(getSize());
|
|
||||||
entry.setExtra(getExtra());
|
|
||||||
entry.setComment(getComment().toString());
|
|
||||||
entry.setSize(getSize());
|
|
||||||
entry.setTime(getTime());
|
|
||||||
this.entry = new SoftReference<JarEntry>(entry);
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AsciiBytes getName() {
|
public AsciiBytes getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasName(String name, String suffix) {
|
||||||
|
return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isDirectory() {
|
public boolean isDirectory() {
|
||||||
return this.name.endsWith(SLASH);
|
return this.name.endsWith(SLASH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getMethod() {
|
public int getMethod() {
|
||||||
return (int) Bytes.littleEndianValue(this.header, 10, 2);
|
return (int) Bytes.littleEndianValue(this.header, 10, 2);
|
||||||
}
|
}
|
||||||
|
@ -176,12 +104,14 @@ public final class JarEntryData {
|
||||||
return Bytes.littleEndianValue(this.header, 16, 4);
|
return Bytes.littleEndianValue(this.header, 16, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCompressedSize() {
|
@Override
|
||||||
return (int) Bytes.littleEndianValue(this.header, 20, 4);
|
public long getCompressedSize() {
|
||||||
|
return Bytes.littleEndianValue(this.header, 20, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSize() {
|
@Override
|
||||||
return (int) Bytes.littleEndianValue(this.header, 24, 4);
|
public long getSize() {
|
||||||
|
return Bytes.littleEndianValue(this.header, 24, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getExtra() {
|
public byte[] getExtra() {
|
||||||
|
@ -192,24 +122,37 @@ public final class JarEntryData {
|
||||||
return this.comment;
|
return this.comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
JarEntryData createFilteredCopy(JarFile jarFile, AsciiBytes name) {
|
@Override
|
||||||
return new JarEntryData(this, jarFile, name);
|
public long getLocalHeaderOffset() {
|
||||||
|
return this.localHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data,
|
||||||
|
int offset) throws IOException {
|
||||||
|
InputStream inputStream = data.getSubsection(offset, data.getSize() - offset)
|
||||||
|
.getInputStream(ResourceAccess.ONCE);
|
||||||
|
try {
|
||||||
|
return fromInputStream(inputStream);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link JarEntryData} instance from the specified input stream.
|
* Create a new {@link CentralDirectoryFileHeader} instance from the specified input
|
||||||
* @param source the source {@link JarFile}
|
* stream.
|
||||||
* @param inputStream the input stream to load data from
|
* @param inputStream the input stream to load data from
|
||||||
* @return a {@link JarEntryData} or {@code null}
|
* @return a {@link CentralDirectoryFileHeader} or {@code null}
|
||||||
* @throws IOException in case of I/O errors
|
* @throws IOException in case of I/O errors
|
||||||
*/
|
*/
|
||||||
static JarEntryData fromInputStream(JarFile source, InputStream inputStream)
|
static CentralDirectoryFileHeader fromInputStream(InputStream inputStream)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
byte[] header = new byte[46];
|
byte[] header = new byte[46];
|
||||||
if (!Bytes.fill(inputStream, header)) {
|
if (!Bytes.fill(inputStream, header)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new JarEntryData(source, header, inputStream);
|
return new CentralDirectoryFileHeader(header, inputStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2015 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
|
||||||
|
*
|
||||||
|
* http://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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the central directory from a JAR file.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see CentralDirectoryVistor
|
||||||
|
*/
|
||||||
|
class CentralDirectoryParser {
|
||||||
|
|
||||||
|
private int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46;
|
||||||
|
|
||||||
|
private final List<CentralDirectoryVistor> vistors = new ArrayList<CentralDirectoryVistor>();
|
||||||
|
|
||||||
|
public <T extends CentralDirectoryVistor> T addVistor(T vistor) {
|
||||||
|
this.vistors.add(vistor);
|
||||||
|
return vistor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the source data, triggering {@link CentralDirectoryVistor vistors}.
|
||||||
|
* @param data the source data
|
||||||
|
* @param skipPrefixBytes if prefix bytes should be skipped
|
||||||
|
* @return The actual archive data without any prefix bytes
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes)
|
||||||
|
throws IOException {
|
||||||
|
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
|
||||||
|
if (skipPrefixBytes) {
|
||||||
|
data = getArchiveData(endRecord, data);
|
||||||
|
}
|
||||||
|
RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data);
|
||||||
|
visitStart(endRecord, centralDirectoryData);
|
||||||
|
InputStream inputStream = centralDirectoryData
|
||||||
|
.getInputStream(ResourceAccess.ONCE);
|
||||||
|
try {
|
||||||
|
int dataOffset = 0;
|
||||||
|
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
|
||||||
|
CentralDirectoryFileHeader fileHeader = CentralDirectoryFileHeader
|
||||||
|
.fromInputStream(inputStream);
|
||||||
|
visitFileHeader(dataOffset, fileHeader);
|
||||||
|
dataOffset += this.CENTRAL_DIRECTORY_HEADER_BASE_SIZE
|
||||||
|
+ fileHeader.getName().length() + fileHeader.getComment().length()
|
||||||
|
+ fileHeader.getExtra().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
visitEnd();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
|
||||||
|
RandomAccessData data) {
|
||||||
|
long offset = endRecord.getStartOfArchive(data);
|
||||||
|
if (offset == 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return data.getSubsection(offset, data.getSize() - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitStart(CentralDirectoryEndRecord endRecord,
|
||||||
|
RandomAccessData centralDirectoryData) {
|
||||||
|
for (CentralDirectoryVistor vistor : this.vistors) {
|
||||||
|
vistor.visitStart(endRecord, centralDirectoryData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitFileHeader(int dataOffset, CentralDirectoryFileHeader fileHeader) {
|
||||||
|
for (CentralDirectoryVistor vistor : this.vistors) {
|
||||||
|
vistor.visitFileHeader(fileHeader, dataOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitEnd() {
|
||||||
|
for (CentralDirectoryVistor vistor : this.vistors) {
|
||||||
|
vistor.visitEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2015 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
|
||||||
|
*
|
||||||
|
* http://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.jar;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback vistor triggered by {@link CentralDirectoryParser}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
interface CentralDirectoryVistor {
|
||||||
|
|
||||||
|
void visitStart(CentralDirectoryEndRecord endRecord,
|
||||||
|
RandomAccessData centralDirectoryData);
|
||||||
|
|
||||||
|
void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset);
|
||||||
|
|
||||||
|
void visitEnd();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2015 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
|
||||||
|
*
|
||||||
|
* http://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.jar;
|
||||||
|
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file header record that has been loaded from a Jar file.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see JarEntry
|
||||||
|
* @see CentralDirectoryFileHeader
|
||||||
|
*/
|
||||||
|
interface FileHeader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the header has the given name.
|
||||||
|
* @param name the name to test
|
||||||
|
* @param suffix an additional suffix (or {@code null})
|
||||||
|
* @return {@code true} if the header has the given name
|
||||||
|
*/
|
||||||
|
boolean hasName(String name, String suffix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the offset of the load file header withing the archive data.
|
||||||
|
* @return the local header offset
|
||||||
|
*/
|
||||||
|
long getLocalHeaderOffset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the compressed size of the entry.
|
||||||
|
* @return the compressed size.
|
||||||
|
*/
|
||||||
|
long getCompressedSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the uncompressed size of the entry.
|
||||||
|
* @return the uncompressed size.
|
||||||
|
*/
|
||||||
|
long getSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the method used to compress the data.
|
||||||
|
* @return the zip compression method
|
||||||
|
* @see ZipEntry#STORED
|
||||||
|
* @see ZipEntry#DEFLATED
|
||||||
|
*/
|
||||||
|
int getMethod();
|
||||||
|
|
||||||
|
}
|
|
@ -29,25 +29,34 @@ import java.util.jar.Manifest;
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public class JarEntry extends java.util.jar.JarEntry {
|
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
||||||
|
|
||||||
private final JarEntryData source;
|
|
||||||
|
|
||||||
private Certificate[] certificates;
|
private Certificate[] certificates;
|
||||||
|
|
||||||
private CodeSigner[] codeSigners;
|
private CodeSigner[] codeSigners;
|
||||||
|
|
||||||
public JarEntry(JarEntryData source) {
|
private final JarFile jarFile;
|
||||||
super(source.getName().toString());
|
|
||||||
this.source = source;
|
private long localHeaderOffset;
|
||||||
|
|
||||||
|
JarEntry(JarFile jarFile, String name, CentralDirectoryFileHeader header) {
|
||||||
|
super(name);
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
this.localHeaderOffset = header.getLocalHeaderOffset();
|
||||||
|
setCompressedSize(header.getCompressedSize());
|
||||||
|
setMethod(header.getMethod());
|
||||||
|
setCrc(header.getCrc());
|
||||||
|
setSize(header.getSize());
|
||||||
|
setExtra(header.getExtra());
|
||||||
|
setComment(header.getComment().toString());
|
||||||
|
setSize(header.getSize());
|
||||||
|
setTime(header.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Return the source {@link JarEntryData} that was used to create this entry.
|
public boolean hasName(String name, String suffix) {
|
||||||
* @return the source of the entry
|
return getName().length() == name.length() + suffix.length()
|
||||||
*/
|
&& getName().startsWith(name) && getName().endsWith(suffix);
|
||||||
public JarEntryData getSource() {
|
|
||||||
return this.source;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,35 +64,40 @@ public class JarEntry extends java.util.jar.JarEntry {
|
||||||
* @return the URL for the entry
|
* @return the URL for the entry
|
||||||
* @throws MalformedURLException if the URL is not valid
|
* @throws MalformedURLException if the URL is not valid
|
||||||
*/
|
*/
|
||||||
public URL getUrl() throws MalformedURLException {
|
URL getUrl() throws MalformedURLException {
|
||||||
return new URL(this.source.getSource().getUrl(), getName());
|
return new URL(this.jarFile.getUrl(), getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Attributes getAttributes() throws IOException {
|
public Attributes getAttributes() throws IOException {
|
||||||
Manifest manifest = this.source.getSource().getManifest();
|
Manifest manifest = this.jarFile.getManifest();
|
||||||
return (manifest == null ? null : manifest.getAttributes(getName()));
|
return (manifest == null ? null : manifest.getAttributes(getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Certificate[] getCertificates() {
|
public Certificate[] getCertificates() {
|
||||||
if (this.source.getSource().isSigned() && this.certificates == null) {
|
if (this.jarFile.isSigned() && this.certificates == null) {
|
||||||
this.source.getSource().setupEntryCertificates();
|
this.jarFile.setupEntryCertificates(this);
|
||||||
}
|
}
|
||||||
return this.certificates;
|
return this.certificates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CodeSigner[] getCodeSigners() {
|
public CodeSigner[] getCodeSigners() {
|
||||||
if (this.source.getSource().isSigned() && this.codeSigners == null) {
|
if (this.jarFile.isSigned() && this.codeSigners == null) {
|
||||||
this.source.getSource().setupEntryCertificates();
|
this.jarFile.setupEntryCertificates(this);
|
||||||
}
|
}
|
||||||
return this.codeSigners;
|
return this.codeSigners;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupCertificates(java.util.jar.JarEntry entry) {
|
void setCertificates(java.util.jar.JarEntry entry) {
|
||||||
this.certificates = entry.getCertificates();
|
this.certificates = entry.getCertificates();
|
||||||
this.codeSigners = entry.getCodeSigners();
|
this.codeSigners = entry.getCodeSigners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLocalHeaderOffset() {
|
||||||
|
return this.localHeaderOffset;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,23 +16,20 @@
|
||||||
|
|
||||||
package org.springframework.boot.loader.jar;
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that can be used to filter and optionally rename jar entries.
|
* Interface that can be used to filter and optionally rename jar entries.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public interface JarEntryFilter {
|
interface JarEntryFilter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the jar entry filter.
|
* Apply the jar entry filter.
|
||||||
* @param name the current entry name. This may be different that the original entry
|
* @param name the current entry name. This may be different that the original entry
|
||||||
* name if a previous filter has been applied
|
* name if a previous filter has been applied
|
||||||
* @param entryData the entry data to filter
|
|
||||||
* @return the new name of the entry or {@code null} if the entry should not be
|
* @return the new name of the entry or {@code null} if the entry should not be
|
||||||
* included.
|
* included.
|
||||||
*/
|
*/
|
||||||
AsciiBytes apply(AsciiBytes name, JarEntryData entryData);
|
AsciiBytes apply(AsciiBytes name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,8 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLStreamHandler;
|
import java.net.URLStreamHandler;
|
||||||
import java.net.URLStreamHandlerFactory;
|
import java.net.URLStreamHandlerFactory;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.jar.JarInputStream;
|
import java.util.jar.JarInputStream;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
|
@ -37,36 +33,30 @@ import java.util.zip.ZipEntry;
|
||||||
import org.springframework.boot.loader.data.RandomAccessData;
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
|
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
|
||||||
* offers the following additional functionality.
|
* offers the following additional functionality.
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created}
|
|
||||||
* from existing files.</li>
|
|
||||||
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
|
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
|
||||||
* on any directory entry.</li>
|
* on any directory entry.</li>
|
||||||
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
|
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
|
||||||
* embedded JAR files (as long as their entry is not compressed).</li>
|
* embedded JAR files (as long as their entry is not compressed).</li>
|
||||||
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
|
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
*/
|
*/
|
||||||
public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryData> {
|
public class JarFile extends java.util.jar.JarFile {
|
||||||
|
|
||||||
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
|
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
|
||||||
|
|
||||||
private static final AsciiBytes MANIFEST_MF = new AsciiBytes("META-INF/MANIFEST.MF");
|
|
||||||
|
|
||||||
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
|
||||||
|
|
||||||
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||||
|
|
||||||
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
|
||||||
|
|
||||||
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
|
||||||
|
|
||||||
|
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
||||||
|
|
||||||
private final RandomAccessDataFile rootFile;
|
private final RandomAccessDataFile rootFile;
|
||||||
|
|
||||||
|
@ -74,17 +64,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
private final RandomAccessData data;
|
private final RandomAccessData data;
|
||||||
|
|
||||||
private final List<JarEntryData> entries;
|
private URL url;
|
||||||
|
|
||||||
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
|
private JarFileEntries entries;
|
||||||
|
|
||||||
private boolean signed;
|
|
||||||
|
|
||||||
private JarEntryData manifestEntry;
|
|
||||||
|
|
||||||
private SoftReference<Manifest> manifest;
|
private SoftReference<Manifest> manifest;
|
||||||
|
|
||||||
private URL url;
|
private boolean signed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link JarFile} backed by the specified file.
|
* Create a new {@link JarFile} backed by the specified file.
|
||||||
|
@ -114,85 +100,43 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
*/
|
*/
|
||||||
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
|
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
|
||||||
RandomAccessData data) throws IOException {
|
RandomAccessData data) throws IOException {
|
||||||
super(rootFile.getFile());
|
this(rootFile, pathFromRoot, data, null);
|
||||||
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
|
|
||||||
this.rootFile = rootFile;
|
|
||||||
this.pathFromRoot = pathFromRoot;
|
|
||||||
this.data = getArchiveData(endRecord, data);
|
|
||||||
this.entries = loadJarEntries(endRecord);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
|
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
|
||||||
RandomAccessData data, List<JarEntryData> entries, JarEntryFilter... filters)
|
RandomAccessData data, JarEntryFilter filter) throws IOException {
|
||||||
throws IOException {
|
|
||||||
super(rootFile.getFile());
|
super(rootFile.getFile());
|
||||||
this.rootFile = rootFile;
|
this.rootFile = rootFile;
|
||||||
this.pathFromRoot = pathFromRoot;
|
this.pathFromRoot = pathFromRoot;
|
||||||
this.data = data;
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
this.entries = filterEntries(entries, filters);
|
this.entries = parser.addVistor(new JarFileEntries(this, filter));
|
||||||
|
parser.addVistor(centralDirectoryVistor());
|
||||||
|
this.data = parser.parse(data, filter == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
|
private CentralDirectoryVistor centralDirectoryVistor() {
|
||||||
RandomAccessData data) {
|
return new CentralDirectoryVistor() {
|
||||||
long offset = endRecord.getStartOfArchive(data);
|
|
||||||
if (offset == 0) {
|
@Override
|
||||||
return data;
|
public void visitStart(CentralDirectoryEndRecord endRecord,
|
||||||
}
|
RandomAccessData centralDirectoryData) {
|
||||||
return data.getSubsection(offset, data.getSize() - offset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<JarEntryData> loadJarEntries(CentralDirectoryEndRecord endRecord)
|
@Override
|
||||||
throws IOException {
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
|
||||||
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
|
int dataOffset) {
|
||||||
int numberOfRecords = endRecord.getNumberOfRecords();
|
AsciiBytes name = fileHeader.getName();
|
||||||
List<JarEntryData> entries = new ArrayList<JarEntryData>(numberOfRecords);
|
if (name.startsWith(META_INF)
|
||||||
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
|
&& name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
||||||
try {
|
JarFile.this.signed = true;
|
||||||
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
|
|
||||||
while (entry != null) {
|
|
||||||
entries.add(entry);
|
|
||||||
processEntry(entry);
|
|
||||||
entry = JarEntryData.fromInputStream(this, inputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<JarEntryData> filterEntries(List<JarEntryData> entries,
|
|
||||||
JarEntryFilter[] filters) {
|
|
||||||
List<JarEntryData> filteredEntries = new ArrayList<JarEntryData>(entries.size());
|
|
||||||
for (JarEntryData entry : entries) {
|
|
||||||
AsciiBytes name = entry.getName();
|
|
||||||
for (JarEntryFilter filter : filters) {
|
|
||||||
name = (filter == null || name == null ? name
|
|
||||||
: filter.apply(name, entry));
|
|
||||||
}
|
|
||||||
if (name != null) {
|
|
||||||
JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
|
|
||||||
filteredEntries.add(filteredCopy);
|
|
||||||
processEntry(filteredCopy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processEntry(JarEntryData entry) {
|
|
||||||
AsciiBytes name = entry.getName();
|
|
||||||
if (name.startsWith(META_INF)) {
|
|
||||||
processMetaInfEntry(name, entry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
|
@Override
|
||||||
if (name.equals(MANIFEST_MF)) {
|
public void visitEnd() {
|
||||||
this.manifestEntry = entry;
|
|
||||||
}
|
|
||||||
if (name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
|
||||||
this.signed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final RandomAccessDataFile getRootJarFile() {
|
protected final RandomAccessDataFile getRootJarFile() {
|
||||||
|
@ -205,12 +149,12 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Manifest getManifest() throws IOException {
|
public Manifest getManifest() throws IOException {
|
||||||
if (this.manifestEntry == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
|
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
|
||||||
if (manifest == null) {
|
if (manifest == null) {
|
||||||
InputStream inputStream = this.manifestEntry.getInputStream();
|
InputStream inputStream = getInputStream(MANIFEST_NAME, ResourceAccess.ONCE);
|
||||||
|
if (inputStream == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
manifest = new Manifest(inputStream);
|
manifest = new Manifest(inputStream);
|
||||||
}
|
}
|
||||||
|
@ -224,7 +168,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<java.util.jar.JarEntry> entries() {
|
public Enumeration<java.util.jar.JarEntry> entries() {
|
||||||
final Iterator<JarEntryData> iterator = iterator();
|
final Iterator<JarEntry> iterator = this.entries.iterator();
|
||||||
return new Enumeration<java.util.jar.JarEntry>() {
|
return new Enumeration<java.util.jar.JarEntry>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -234,14 +178,10 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public java.util.jar.JarEntry nextElement() {
|
public java.util.jar.JarEntry nextElement() {
|
||||||
return iterator.next().asJarEntry();
|
return iterator.next();
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
};
|
||||||
public Iterator<JarEntryData> iterator() {
|
|
||||||
return this.entries.iterator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -251,72 +191,24 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ZipEntry getEntry(String name) {
|
public ZipEntry getEntry(String name) {
|
||||||
JarEntryData jarEntryData = getJarEntryData(name);
|
return this.entries.getEntry(name);
|
||||||
return (jarEntryData == null ? null : jarEntryData.asJarEntry());
|
|
||||||
}
|
|
||||||
|
|
||||||
public JarEntryData getJarEntryData(String name) {
|
|
||||||
if (name == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getJarEntryData(new AsciiBytes(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public JarEntryData getJarEntryData(AsciiBytes name) {
|
|
||||||
if (name == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<AsciiBytes, JarEntryData> entriesByName = (this.entriesByName == null ? null
|
|
||||||
: this.entriesByName.get());
|
|
||||||
if (entriesByName == null) {
|
|
||||||
entriesByName = new HashMap<AsciiBytes, JarEntryData>();
|
|
||||||
for (JarEntryData entry : this.entries) {
|
|
||||||
entriesByName.put(entry.getName(), entry);
|
|
||||||
}
|
|
||||||
this.entriesByName = new SoftReference<Map<AsciiBytes, JarEntryData>>(
|
|
||||||
entriesByName);
|
|
||||||
}
|
|
||||||
|
|
||||||
JarEntryData entryData = entriesByName.get(name);
|
|
||||||
if (entryData == null && !name.endsWith(SLASH)) {
|
|
||||||
entryData = entriesByName.get(name.append(SLASH));
|
|
||||||
}
|
|
||||||
return entryData;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isSigned() {
|
|
||||||
return this.signed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupEntryCertificates() {
|
|
||||||
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
|
|
||||||
// happening that often.
|
|
||||||
try {
|
|
||||||
JarInputStream inputStream = new JarInputStream(
|
|
||||||
getData().getInputStream(ResourceAccess.ONCE));
|
|
||||||
try {
|
|
||||||
java.util.jar.JarEntry entry = inputStream.getNextJarEntry();
|
|
||||||
while (entry != null) {
|
|
||||||
inputStream.closeEntry();
|
|
||||||
JarEntry jarEntry = getJarEntry(entry.getName());
|
|
||||||
if (jarEntry != null) {
|
|
||||||
jarEntry.setupCertificates(entry);
|
|
||||||
}
|
|
||||||
entry = inputStream.getNextJarEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
throw new IllegalStateException(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||||
return getContainedEntry(ze).getSource().getInputStream();
|
return getInputStream(ze, ResourceAccess.PER_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream(ZipEntry ze, ResourceAccess access)
|
||||||
|
throws IOException {
|
||||||
|
if (ze instanceof JarEntry) {
|
||||||
|
return this.entries.getInputStream((JarEntry) ze, access);
|
||||||
|
}
|
||||||
|
return getInputStream(ze == null ? null : ze.getName(), access);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream getInputStream(String name, ResourceAccess access) throws IOException {
|
||||||
|
return this.entries.getInputStream(name, access);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -327,42 +219,37 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
*/
|
*/
|
||||||
public synchronized JarFile getNestedJarFile(final ZipEntry entry)
|
public synchronized JarFile getNestedJarFile(final ZipEntry entry)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return getNestedJarFile(getContainedEntry(entry).getSource());
|
return getNestedJarFile((JarEntry) entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a nested {@link JarFile} loaded from the specified entry.
|
* Return a nested {@link JarFile} loaded from the specified entry.
|
||||||
* @param sourceEntry the zip entry
|
* @param entry the zip entry
|
||||||
* @return a {@link JarFile} for the entry
|
* @return a {@link JarFile} for the entry
|
||||||
* @throws IOException if the nested jar file cannot be read
|
* @throws IOException if the nested jar file cannot be read
|
||||||
*/
|
*/
|
||||||
public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
|
public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
|
||||||
throws IOException {
|
|
||||||
try {
|
try {
|
||||||
if (sourceEntry.nestedJar == null) {
|
return createJarFileFromEntry(entry);
|
||||||
sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
|
|
||||||
}
|
|
||||||
return sourceEntry.nestedJar;
|
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Unable to open nested jar file '" + sourceEntry.getName() + "'", ex);
|
"Unable to open nested jar file '" + entry.getName() + "'", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JarFile createJarFileFromEntry(JarEntryData sourceEntry) throws IOException {
|
private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
|
||||||
if (sourceEntry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
return createJarFileFromDirectoryEntry(sourceEntry);
|
return createJarFileFromDirectoryEntry(entry);
|
||||||
}
|
}
|
||||||
return createJarFileFromFileEntry(sourceEntry);
|
return createJarFileFromFileEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
|
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
|
||||||
throws IOException {
|
final AsciiBytes sourceName = new AsciiBytes(entry.getName());
|
||||||
final AsciiBytes sourceName = sourceEntry.getName();
|
|
||||||
JarEntryFilter filter = new JarEntryFilter() {
|
JarEntryFilter filter = new JarEntryFilter() {
|
||||||
@Override
|
@Override
|
||||||
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
public AsciiBytes apply(AsciiBytes name) {
|
||||||
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
||||||
return name.substring(sourceName.length());
|
return name.substring(sourceName.length());
|
||||||
}
|
}
|
||||||
|
@ -371,40 +258,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
};
|
};
|
||||||
return new JarFile(this.rootFile,
|
return new JarFile(this.rootFile,
|
||||||
this.pathFromRoot + "!/"
|
this.pathFromRoot + "!/"
|
||||||
+ sourceEntry.getName().substring(0, sourceName.length() - 1),
|
+ entry.getName().substring(0, sourceName.length() - 1),
|
||||||
this.data, this.entries, filter);
|
this.data, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
|
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
|
||||||
throws IOException {
|
if (entry.getMethod() != ZipEntry.STORED) {
|
||||||
if (sourceEntry.getMethod() != ZipEntry.STORED) {
|
|
||||||
throw new IllegalStateException("Unable to open nested entry '"
|
throw new IllegalStateException("Unable to open nested entry '"
|
||||||
+ sourceEntry.getName() + "'. It has been compressed and nested "
|
+ entry.getName() + "'. It has been compressed and nested "
|
||||||
+ "jar files must be stored without compression. Please check the "
|
+ "jar files must be stored without compression. Please check the "
|
||||||
+ "mechanism used to create your executable jar file");
|
+ "mechanism used to create your executable jar file");
|
||||||
}
|
}
|
||||||
return new JarFile(this.rootFile,
|
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
|
||||||
this.pathFromRoot + "!/" + sourceEntry.getName(), sourceEntry.getData());
|
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
|
||||||
}
|
entryData);
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a new jar based on the filtered contents of this file.
|
|
||||||
* @param filters the set of jar entry filters to be applied
|
|
||||||
* @return a filtered {@link JarFile}
|
|
||||||
* @throws IOException if the jar file cannot be read
|
|
||||||
*/
|
|
||||||
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
|
|
||||||
throws IOException {
|
|
||||||
return new JarFile(this.rootFile, this.pathFromRoot, this.data, this.entries,
|
|
||||||
filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
|
|
||||||
if (zipEntry instanceof JarEntry
|
|
||||||
&& ((JarEntry) zipEntry).getSource().getSource() == this) {
|
|
||||||
return (JarEntry) zipEntry;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("ZipEntry must be contained in this file");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -444,6 +311,42 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
||||||
return this.rootFile.getFile() + path;
|
return this.rootFile.getFile() + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isSigned() {
|
||||||
|
return this.signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupEntryCertificates(JarEntry entry) {
|
||||||
|
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
|
||||||
|
// happening that often.
|
||||||
|
try {
|
||||||
|
JarInputStream inputStream = new JarInputStream(
|
||||||
|
getData().getInputStream(ResourceAccess.ONCE));
|
||||||
|
try {
|
||||||
|
java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry();
|
||||||
|
while (certEntry != null) {
|
||||||
|
inputStream.closeEntry();
|
||||||
|
if (entry.getName().equals(certEntry.getName())) {
|
||||||
|
setCertificates(entry, certEntry);
|
||||||
|
}
|
||||||
|
setCertificates(getJarEntry(certEntry.getName()), certEntry);
|
||||||
|
certEntry = inputStream.getNextJarEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) {
|
||||||
|
if (entry != null) {
|
||||||
|
entry.setCertificates(certEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
|
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
|
||||||
* {@link URLStreamHandler} will be located to deal with jar URLs.
|
* {@link URLStreamHandler} will be located to deal with jar URLs.
|
||||||
|
|
|
@ -0,0 +1,301 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2015 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
|
||||||
|
*
|
||||||
|
* http://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.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to entries from a {@link JarFile}. In order to reduce memory
|
||||||
|
* consumption entry details are stored using int arrays. The {@code hashCodes} array
|
||||||
|
* stores the hash code of the entry name, the {@code centralDirectoryOffsets} provides
|
||||||
|
* the offset to the central directory record and {@code positions} provides the original
|
||||||
|
* order position of the entry. The arrays are stored in hashCode order so that a binary
|
||||||
|
* search can be used to find a name.
|
||||||
|
* <p>
|
||||||
|
* A typical Spring Boot application will have somewhere in the region of 10,500 entries
|
||||||
|
* which should consume about 122K.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class JarFileEntries implements CentralDirectoryVistor, Iterable<JarEntry> {
|
||||||
|
|
||||||
|
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
||||||
|
|
||||||
|
private static final String SLASH = "/";
|
||||||
|
|
||||||
|
private static final String NO_SUFFIX = "";
|
||||||
|
|
||||||
|
protected static final int ENTRY_CACHE_SIZE = 25;
|
||||||
|
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
private final JarEntryFilter filter;
|
||||||
|
|
||||||
|
private RandomAccessData centralDirectoryData;
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
private int[] hashCodes;
|
||||||
|
|
||||||
|
private int[] centralDirectoryOffsets;
|
||||||
|
|
||||||
|
private int[] positions;
|
||||||
|
|
||||||
|
private final Map<Integer, JarEntry> entriesCache = Collections
|
||||||
|
.synchronizedMap(new LinkedHashMap<Integer, JarEntry>(16, 0.75f, true) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<Integer, JarEntry> eldest) {
|
||||||
|
if (JarFileEntries.this.jarFile.isSigned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return size() >= ENTRY_CACHE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord,
|
||||||
|
RandomAccessData centralDirectoryData) {
|
||||||
|
int maxSize = endRecord.getNumberOfRecords();
|
||||||
|
this.centralDirectoryData = centralDirectoryData;
|
||||||
|
this.hashCodes = new int[maxSize];
|
||||||
|
this.centralDirectoryOffsets = new int[maxSize];
|
||||||
|
this.positions = new int[maxSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) {
|
||||||
|
AsciiBytes name = applyFilter(fileHeader.getName());
|
||||||
|
if (name != null) {
|
||||||
|
add(name, fileHeader, dataOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(AsciiBytes name, CentralDirectoryFileHeader fileHeader,
|
||||||
|
int dataOffset) {
|
||||||
|
this.hashCodes[this.size] = name.hashCode();
|
||||||
|
this.centralDirectoryOffsets[this.size] = dataOffset;
|
||||||
|
this.positions[this.size] = this.size;
|
||||||
|
this.size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
sort(0, this.size - 1);
|
||||||
|
int[] positions = this.positions;
|
||||||
|
this.positions = new int[positions.length];
|
||||||
|
for (int i = 0; i < this.size; i++) {
|
||||||
|
this.positions[positions[i]] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sort(int left, int right) {
|
||||||
|
// Quick sort algorithm, uses hashCodes as the source but sorts all arrays
|
||||||
|
if (left < right) {
|
||||||
|
int pivot = this.hashCodes[left + (right - left) / 2];
|
||||||
|
int i = left;
|
||||||
|
int j = right;
|
||||||
|
while (i <= j) {
|
||||||
|
while (this.hashCodes[i] < pivot) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (this.hashCodes[j] > pivot) {
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
if (i <= j) {
|
||||||
|
swap(i, j);
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left < j) {
|
||||||
|
sort(left, j);
|
||||||
|
}
|
||||||
|
if (right > i) {
|
||||||
|
sort(i, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void swap(int i, int j) {
|
||||||
|
swap(this.hashCodes, i, j);
|
||||||
|
swap(this.centralDirectoryOffsets, i, j);
|
||||||
|
swap(this.positions, i, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void swap(int[] array, int i, int j) {
|
||||||
|
int temp = array[i];
|
||||||
|
array[i] = array[j];
|
||||||
|
array[j] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<JarEntry> iterator() {
|
||||||
|
return new EntryIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarEntry getEntry(String name) {
|
||||||
|
return getEntry(name, JarEntry.class, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream(String name, ResourceAccess access)
|
||||||
|
throws IOException {
|
||||||
|
FileHeader entry = getEntry(name, FileHeader.class, false);
|
||||||
|
return getInputStream(entry, access);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream(FileHeader entry, ResourceAccess access)
|
||||||
|
throws IOException {
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
InputStream inputStream = getEntryData(entry).getInputStream(access);
|
||||||
|
if (entry.getMethod() == ZipEntry.DEFLATED) {
|
||||||
|
inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize());
|
||||||
|
}
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RandomAccessData getEntryData(String name) throws IOException {
|
||||||
|
FileHeader entry = getEntry(name, FileHeader.class, false);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getEntryData(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RandomAccessData getEntryData(FileHeader entry) throws IOException {
|
||||||
|
// aspectjrt-1.7.4.jar has a different ext bytes length in the
|
||||||
|
// local directory to the central directory. We need to re-read
|
||||||
|
// here to skip them
|
||||||
|
RandomAccessData data = this.jarFile.getData();
|
||||||
|
byte[] localHeader = Bytes.get(
|
||||||
|
data.getSubsection(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE));
|
||||||
|
long nameLength = Bytes.littleEndianValue(localHeader, 26, 2);
|
||||||
|
long extraLength = Bytes.littleEndianValue(localHeader, 28, 2);
|
||||||
|
return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE
|
||||||
|
+ nameLength + extraLength, entry.getCompressedSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends FileHeader> T getEntry(String name, Class<T> type,
|
||||||
|
boolean cacheEntry) {
|
||||||
|
int hashCode = AsciiBytes.hashCode(name);
|
||||||
|
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
|
||||||
|
if (entry == null) {
|
||||||
|
hashCode = AsciiBytes.hashCode(hashCode, SLASH);
|
||||||
|
entry = getEntry(hashCode, name, SLASH, type, cacheEntry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends FileHeader> T getEntry(int hashCode, String name, String suffix,
|
||||||
|
Class<T> type, boolean cacheEntry) {
|
||||||
|
int index = getFirstIndex(hashCode);
|
||||||
|
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
|
||||||
|
T entry = getEntry(index, type, cacheEntry);
|
||||||
|
if (entry.hasName(name, suffix)) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends FileHeader> T getEntry(int index, Class<T> type,
|
||||||
|
boolean cacheEntry) {
|
||||||
|
JarEntry entry = this.entriesCache.get(index);
|
||||||
|
if (entry != null) {
|
||||||
|
return (T) entry;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CentralDirectoryFileHeader header = CentralDirectoryFileHeader
|
||||||
|
.fromRandomAccessData(this.centralDirectoryData,
|
||||||
|
this.centralDirectoryOffsets[index]);
|
||||||
|
if (FileHeader.class.equals(type)) {
|
||||||
|
// No need to convert
|
||||||
|
return (T) header;
|
||||||
|
}
|
||||||
|
entry = new JarEntry(this.jarFile, applyFilter(header.getName()).toString(),
|
||||||
|
header);
|
||||||
|
if (cacheEntry) {
|
||||||
|
this.entriesCache.put(index, entry);
|
||||||
|
}
|
||||||
|
return (T) entry;
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getFirstIndex(int hashCode) {
|
||||||
|
int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode);
|
||||||
|
if (index < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
while (index > 0 && this.hashCodes[index - 1] == hashCode) {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsciiBytes applyFilter(AsciiBytes name) {
|
||||||
|
return (this.filter == null ? name : this.filter.apply(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator for contained entries.
|
||||||
|
*/
|
||||||
|
private class EntryIterator implements Iterator<JarEntry> {
|
||||||
|
|
||||||
|
private int index = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.index < JarFileEntries.this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry next() {
|
||||||
|
if (!hasNext()) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
int entryIndex = JarFileEntries.this.positions[this.index];
|
||||||
|
this.index++;
|
||||||
|
return getEntry(entryIndex, JarEntry.class, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,9 +24,6 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.net.URLStreamHandler;
|
import java.net.URLStreamHandler;
|
||||||
import java.util.jar.Manifest;
|
|
||||||
|
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
|
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
|
||||||
|
@ -63,11 +60,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
private final JarFile jarFile;
|
private final JarFile jarFile;
|
||||||
|
|
||||||
private JarEntryData jarEntryData;
|
|
||||||
|
|
||||||
private URL jarFileUrl;
|
private URL jarFileUrl;
|
||||||
|
|
||||||
private JarEntryName jarEntryName;
|
private final JarEntryName jarEntryName;
|
||||||
|
|
||||||
|
private JarEntry jarEntry;
|
||||||
|
|
||||||
protected JarURLConnection(URL url, JarFile jarFile) throws IOException {
|
protected JarURLConnection(URL url, JarFile jarFile) throws IOException {
|
||||||
// What we pass to super is ultimately ignored
|
// What we pass to super is ultimately ignored
|
||||||
|
@ -100,10 +97,9 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connect() throws IOException {
|
public void connect() throws IOException {
|
||||||
if (!this.jarEntryName.isEmpty()) {
|
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
|
||||||
this.jarEntryData = this.jarFile
|
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
|
||||||
.getJarEntryData(this.jarEntryName.asAsciiBytes());
|
if (this.jarEntry == null) {
|
||||||
if (this.jarEntryData == null) {
|
|
||||||
throwFileNotFound(this.jarEntryName, this.jarFile);
|
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,16 +115,6 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
"JAR entry " + entry + " not found in " + jarFile.getName());
|
"JAR entry " + entry + " not found in " + jarFile.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Manifest getManifest() throws IOException {
|
|
||||||
try {
|
|
||||||
return super.getManifest();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
this.connected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JarFile getJarFile() throws IOException {
|
public JarFile getJarFile() throws IOException {
|
||||||
connect();
|
connect();
|
||||||
|
@ -161,8 +147,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JarEntry getJarEntry() throws IOException {
|
public JarEntry getJarEntry() throws IOException {
|
||||||
|
if (this.jarEntryName.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
connect();
|
connect();
|
||||||
return (this.jarEntryData == null ? null : this.jarEntryData.asJarEntry());
|
return this.jarEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -172,22 +161,26 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() throws IOException {
|
public InputStream getInputStream() throws IOException {
|
||||||
connect();
|
|
||||||
if (this.jarEntryName.isEmpty()) {
|
if (this.jarEntryName.isEmpty()) {
|
||||||
throw new IOException("no entry name specified");
|
throw new IOException("no entry name specified");
|
||||||
}
|
}
|
||||||
return this.jarEntryData.getInputStream();
|
connect();
|
||||||
|
InputStream inputStream = this.jarFile.getInputStream(this.jarEntry);
|
||||||
|
if (inputStream == null) {
|
||||||
|
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||||
|
}
|
||||||
|
return inputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getContentLength() {
|
public int getContentLength() {
|
||||||
try {
|
try {
|
||||||
connect();
|
if (this.jarEntryName.isEmpty()) {
|
||||||
if (this.jarEntryData != null) {
|
|
||||||
return this.jarEntryData.getSize();
|
|
||||||
}
|
|
||||||
return this.jarFile.size();
|
return this.jarFile.size();
|
||||||
}
|
}
|
||||||
|
JarEntry entry = getJarEntry();
|
||||||
|
return (entry == null ? -1 : (int) entry.getSize());
|
||||||
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -196,7 +189,7 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
@Override
|
@Override
|
||||||
public Object getContent() throws IOException {
|
public Object getContent() throws IOException {
|
||||||
connect();
|
connect();
|
||||||
return (this.jarEntryData == null ? this.jarFile : super.getContent());
|
return (this.jarEntryName.isEmpty() ? this.jarFile : super.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -213,7 +206,7 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
*/
|
*/
|
||||||
private static class JarEntryName {
|
private static class JarEntryName {
|
||||||
|
|
||||||
private final AsciiBytes name;
|
private final String name;
|
||||||
|
|
||||||
private String contentType;
|
private String contentType;
|
||||||
|
|
||||||
|
@ -221,26 +214,26 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
this.name = decode(spec);
|
this.name = decode(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AsciiBytes decode(String source) {
|
private String decode(String source) {
|
||||||
int length = (source == null ? 0 : source.length());
|
int length = (source == null ? 0 : source.length());
|
||||||
if ((length == 0) || (source.indexOf('%') < 0)) {
|
if ((length == 0) || (source.indexOf('%') < 0)) {
|
||||||
return new AsciiBytes(source);
|
return new AsciiBytes(source).toString();
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(length);
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int ch = source.charAt(i);
|
int c = source.charAt(i);
|
||||||
if (ch == '%') {
|
if (c == '%') {
|
||||||
if ((i + 2) >= length) {
|
if ((i + 2) >= length) {
|
||||||
throw new IllegalArgumentException("Invalid encoded sequence \""
|
throw new IllegalArgumentException("Invalid encoded sequence \""
|
||||||
+ source.substring(i) + "\"");
|
+ source.substring(i) + "\"");
|
||||||
}
|
}
|
||||||
ch = decodeEscapeSequence(source, i);
|
c = decodeEscapeSequence(source, i);
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
bos.write(ch);
|
outputStream.write(c);
|
||||||
}
|
}
|
||||||
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
||||||
return new AsciiBytes(bos.toByteArray());
|
return new AsciiBytes(outputStream.toByteArray()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private char decodeEscapeSequence(String source, int i) {
|
private char decodeEscapeSequence(String source, int i) {
|
||||||
|
@ -258,10 +251,6 @@ class JarURLConnection extends java.net.JarURLConnection {
|
||||||
return this.name.toString();
|
return this.name.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AsciiBytes asAsciiBytes() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return this.name.length() == 0;
|
return this.name.length() == 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,11 +37,9 @@ import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import org.springframework.boot.loader.TestJarCreator;
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.greaterThan;
|
import static org.hamcrest.Matchers.greaterThan;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
@ -130,27 +128,6 @@ public class ExplodedArchiveTests {
|
||||||
equalTo("file:" + this.rootFolder.toURI().getPath() + "d/"));
|
equalTo("file:" + this.rootFolder.toURI().getPath() + "d/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getFilteredArchive() throws Exception {
|
|
||||||
Archive filteredArchive = this.archive
|
|
||||||
.getFilteredArchive(new Archive.EntryRenameFilter() {
|
|
||||||
@Override
|
|
||||||
public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
|
|
||||||
if (entryName.toString().equals("1.dat")) {
|
|
||||||
return entryName;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Map<String, Entry> entries = getEntriesMap(filteredArchive);
|
|
||||||
assertThat(entries.size(), equalTo(1));
|
|
||||||
URLClassLoader classLoader = new URLClassLoader(
|
|
||||||
new URL[] { filteredArchive.getUrl() });
|
|
||||||
assertThat(classLoader.getResourceAsStream("1.dat").read(), equalTo(1));
|
|
||||||
assertThat(classLoader.getResourceAsStream("2.dat"), nullValue());
|
|
||||||
classLoader.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNonRecursiveEntriesForRoot() throws Exception {
|
public void getNonRecursiveEntriesForRoot() throws Exception {
|
||||||
ExplodedArchive archive = new ExplodedArchive(new File("/"), false);
|
ExplodedArchive archive = new ExplodedArchive(new File("/"), false);
|
||||||
|
@ -198,7 +175,7 @@ public class ExplodedArchiveTests {
|
||||||
|
|
||||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||||
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
||||||
for (Archive.Entry entry : archive.getEntries()) {
|
for (Archive.Entry entry : archive) {
|
||||||
entries.put(entry.getName().toString(), entry);
|
entries.put(entry.getName().toString(), entry);
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import org.springframework.boot.loader.TestJarCreator;
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.endsWith;
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -126,25 +125,9 @@ public class JarFileArchiveTests {
|
||||||
assertThat(nested.getParent(), is(equalTo(anotherNested.getParent())));
|
assertThat(nested.getParent(), is(equalTo(anotherNested.getParent())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getFilteredArchive() throws Exception {
|
|
||||||
Archive filteredArchive = this.archive
|
|
||||||
.getFilteredArchive(new Archive.EntryRenameFilter() {
|
|
||||||
@Override
|
|
||||||
public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
|
|
||||||
if (entryName.toString().equals("1.dat")) {
|
|
||||||
return entryName;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Map<String, Entry> entries = getEntriesMap(filteredArchive);
|
|
||||||
assertThat(entries.size(), equalTo(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||||
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
||||||
for (Archive.Entry entry : archive.getEntries()) {
|
for (Archive.Entry entry : archive) {
|
||||||
entries.put(entry.getName().toString(), entry);
|
entries.put(entry.getName().toString(), entry);
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
|
|
|
@ -14,14 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.loader;
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
|
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
@ -138,8 +136,22 @@ public class AsciiBytesTests {
|
||||||
assertThat(bc, equalTo(bc));
|
assertThat(bc, equalTo(bc));
|
||||||
assertThat(bc, equalTo(bc_substring));
|
assertThat(bc, equalTo(bc_substring));
|
||||||
assertThat(bc, equalTo(bc_string));
|
assertThat(bc, equalTo(bc_string));
|
||||||
|
|
||||||
assertThat(bc.hashCode(), not(equalTo(abcd.hashCode())));
|
assertThat(bc.hashCode(), not(equalTo(abcd.hashCode())));
|
||||||
assertThat(bc, not(equalTo(abcd)));
|
assertThat(bc, not(equalTo(abcd)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hashCodeSameAsString() throws Exception {
|
||||||
|
String s = "abcABC123xyz!";
|
||||||
|
AsciiBytes a = new AsciiBytes(s);
|
||||||
|
assertThat(s.hashCode(), equalTo(a.hashCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hashCodeSameAsStringWithSpecial() throws Exception {
|
||||||
|
String s = "special/\u00EB.dat";
|
||||||
|
AsciiBytes a = new AsciiBytes(s);
|
||||||
|
assertThat(s.hashCode(), equalTo(a.hashCode()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2015 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
|
||||||
|
*
|
||||||
|
* http://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.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.InOrder;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CentralDirectoryParser}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class CentralDirectoryParserTests {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private File jarFile;
|
||||||
|
|
||||||
|
private RandomAccessData jarData;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
|
this.jarFile = this.temporaryFolder.newFile();
|
||||||
|
TestJarCreator.createTestJar(this.jarFile);
|
||||||
|
this.jarData = new RandomAccessDataFile(this.jarFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vistsInOrder() throws Exception {
|
||||||
|
CentralDirectoryVistor vistor = mock(CentralDirectoryVistor.class);
|
||||||
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
|
parser.addVistor(vistor);
|
||||||
|
parser.parse(this.jarData, false);
|
||||||
|
InOrder ordered = inOrder(vistor);
|
||||||
|
ordered.verify(vistor).visitStart(any(CentralDirectoryEndRecord.class),
|
||||||
|
any(RandomAccessData.class));
|
||||||
|
ordered.verify(vistor, atLeastOnce())
|
||||||
|
.visitFileHeader(any(CentralDirectoryFileHeader.class), anyInt());
|
||||||
|
ordered.verify(vistor).visitEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vistRecords() throws Exception {
|
||||||
|
Collector collector = new Collector();
|
||||||
|
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||||
|
parser.addVistor(collector);
|
||||||
|
parser.parse(this.jarData, false);
|
||||||
|
Iterator<CentralDirectoryFileHeader> headers = collector.getHeaders().iterator();
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("META-INF/"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("META-INF/MANIFEST.MF"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("1.dat"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("2.dat"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("d/"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("d/9.dat"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("special/"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("special/\u00EB.dat"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("nested.jar"));
|
||||||
|
assertThat(headers.next().getName().toString(), equalTo("another-nested.jar"));
|
||||||
|
assertThat(headers.hasNext(), equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Collector implements CentralDirectoryVistor {
|
||||||
|
|
||||||
|
private List<CentralDirectoryFileHeader> headers = new ArrayList<CentralDirectoryFileHeader>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitStart(CentralDirectoryEndRecord endRecord,
|
||||||
|
RandomAccessData centralDirectoryData) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
|
||||||
|
int dataOffset) {
|
||||||
|
this.headers.add(fileHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CentralDirectoryFileHeader> getHeaders() {
|
||||||
|
return this.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,7 +38,6 @@ import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import org.springframework.boot.loader.TestJarCreator;
|
import org.springframework.boot.loader.TestJarCreator;
|
||||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
import org.springframework.boot.loader.util.AsciiBytes;
|
|
||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.util.StreamUtils;
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
|
@ -349,27 +348,6 @@ public class JarFileTests {
|
||||||
assertThat(inputStream.read(), equalTo(-1));
|
assertThat(inputStream.read(), equalTo(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getFilteredJarFile() throws Exception {
|
|
||||||
JarFile filteredJarFile = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
|
|
||||||
@Override
|
|
||||||
public AsciiBytes apply(AsciiBytes entryName, JarEntryData entry) {
|
|
||||||
if (entryName.toString().equals("1.dat")) {
|
|
||||||
return new AsciiBytes("x.dat");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Enumeration<java.util.jar.JarEntry> entries = filteredJarFile.entries();
|
|
||||||
assertThat(entries.nextElement().getName(), equalTo("x.dat"));
|
|
||||||
assertThat(entries.hasMoreElements(), equalTo(false));
|
|
||||||
|
|
||||||
InputStream inputStream = filteredJarFile
|
|
||||||
.getInputStream(filteredJarFile.getEntry("x.dat"));
|
|
||||||
assertThat(inputStream.read(), equalTo(1));
|
|
||||||
assertThat(inputStream.read(), equalTo(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sensibleToString() throws Exception {
|
public void sensibleToString() throws Exception {
|
||||||
assertThat(this.jarFile.toString(), equalTo(this.rootJarFile.getPath()));
|
assertThat(this.jarFile.toString(), equalTo(this.rootJarFile.getPath()));
|
||||||
|
|
Loading…
Reference in New Issue