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.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||
|
@ -66,7 +67,16 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.springframework.boot.loader;
|
|||
import java.util.List;
|
||||
|
||||
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
|
||||
|
@ -29,7 +28,7 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
|||
*/
|
||||
public class JarLauncher extends ExecutableArchiveLauncher {
|
||||
|
||||
private static final AsciiBytes LIB = new AsciiBytes("lib/");
|
||||
private static final String LIB = "lib/";
|
||||
|
||||
public JarLauncher() {
|
||||
}
|
||||
|
|
|
@ -204,20 +204,15 @@ public class LaunchedURLClassLoader extends URLClassLoader {
|
|||
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
|
||||
@Override
|
||||
public Object run() throws ClassNotFoundException {
|
||||
String path = name.replace('.', '/').concat(".class");
|
||||
for (URL url : getURLs()) {
|
||||
try {
|
||||
if (url.getContent() instanceof JarFile) {
|
||||
JarFile jarFile = (JarFile) url.getContent();
|
||||
// Check the jar entry data before needlessly creating the
|
||||
// manifest
|
||||
if (jarFile.getJarEntryData(path) != null
|
||||
&& jarFile.getManifest() != null) {
|
||||
if (jarFile.getManifest() != null) {
|
||||
definePackage(packageName, jarFile.getManifest(),
|
||||
url);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
|
|
@ -21,14 +21,15 @@ import java.io.FileInputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
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.EntryFilter;
|
||||
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.util.AsciiBytes;
|
||||
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";
|
||||
|
||||
private static final List<String> DEFAULT_PATHS = Arrays.asList();
|
||||
|
||||
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
|
||||
|
||||
private static final URL[] EMPTY_URLS = {};
|
||||
|
@ -132,7 +129,7 @@ public class PropertiesLauncher extends Launcher {
|
|||
|
||||
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();
|
||||
|
||||
|
@ -168,7 +165,6 @@ public class PropertiesLauncher extends Launcher {
|
|||
config = SystemPropertyUtils.resolvePlaceholders(
|
||||
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config));
|
||||
InputStream resource = getResource(config);
|
||||
|
||||
if (resource != null) {
|
||||
log("Found: " + config);
|
||||
try {
|
||||
|
@ -353,7 +349,6 @@ public class PropertiesLauncher extends Launcher {
|
|||
@SuppressWarnings("unchecked")
|
||||
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
|
||||
String loaderClassName) throws Exception {
|
||||
|
||||
Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
|
||||
.forName(loaderClassName, true, parent);
|
||||
|
||||
|
@ -363,7 +358,6 @@ public class PropertiesLauncher extends Launcher {
|
|||
catch (NoSuchMethodException ex) {
|
||||
// Ignore and try with URLs
|
||||
}
|
||||
|
||||
try {
|
||||
return loaderClass.getConstructor(URL[].class, ClassLoader.class)
|
||||
.newInstance(new URL[0], parent);
|
||||
|
@ -371,7 +365,6 @@ public class PropertiesLauncher extends Launcher {
|
|||
catch (NoSuchMethodException ex) {
|
||||
// Ignore and try without any arguments
|
||||
}
|
||||
|
||||
return loaderClass.newInstance();
|
||||
}
|
||||
|
||||
|
@ -384,21 +377,18 @@ public class PropertiesLauncher extends Launcher {
|
|||
manifestKey = propertyKey.replace(".", "-");
|
||||
manifestKey = toCamelCase(manifestKey);
|
||||
}
|
||||
|
||||
String property = SystemPropertyUtils.getProperty(propertyKey);
|
||||
if (property != null) {
|
||||
String value = SystemPropertyUtils.resolvePlaceholders(property);
|
||||
log("Property '" + propertyKey + "' from environment: " + value);
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.properties.containsKey(propertyKey)) {
|
||||
String value = SystemPropertyUtils
|
||||
.resolvePlaceholders(this.properties.getProperty(propertyKey));
|
||||
log("Property '" + propertyKey + "' from properties: " + value);
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
// Prefer home dir for MANIFEST if there is one
|
||||
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
|
||||
|
@ -412,7 +402,6 @@ public class PropertiesLauncher extends Launcher {
|
|||
catch (IllegalStateException ex) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Otherwise try the parent archive
|
||||
Manifest manifest = createArchive().getManifest();
|
||||
if (manifest != null) {
|
||||
|
@ -478,7 +467,7 @@ public class PropertiesLauncher extends Launcher {
|
|||
return null;
|
||||
}
|
||||
|
||||
private Archive getNestedArchive(final String root) throws Exception {
|
||||
private Archive getNestedArchive(String root) throws Exception {
|
||||
if (root.startsWith("/")
|
||||
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
|
||||
// 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
|
||||
* (e.g. "lib/").
|
||||
*/
|
||||
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
|
||||
|
||||
private final AsciiBytes prefix;
|
||||
private final String prefix;
|
||||
|
||||
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
|
||||
|
||||
private PrefixMatchingArchiveFilter(String prefix) {
|
||||
this.prefix = new AsciiBytes(prefix);
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Entry 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;
|
||||
|
||||
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.
|
||||
|
@ -29,14 +28,13 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
|||
*/
|
||||
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
|
||||
.append("lib-provided/");
|
||||
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
|
||||
|
||||
public WarLauncher() {
|
||||
super();
|
||||
|
|
|
@ -19,12 +19,10 @@ package org.springframework.boot.loader.archive;
|
|||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.Launcher;
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
|
||||
/**
|
||||
* An archive that can be launched by the {@link Launcher}.
|
||||
|
@ -32,56 +30,21 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
|||
* @author Phillip Webb
|
||||
* @see JarFileArchive
|
||||
*/
|
||||
public abstract class Archive {
|
||||
public interface Archive extends Iterable<Archive.Entry> {
|
||||
|
||||
/**
|
||||
* Returns a URL that can be used to load the archive.
|
||||
* @return the archive URL
|
||||
* @throws MalformedURLException if the URL is malformed
|
||||
*/
|
||||
public abstract 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";
|
||||
}
|
||||
}
|
||||
URL getUrl() throws MalformedURLException;
|
||||
|
||||
/**
|
||||
* Returns the manifest of the archive.
|
||||
* @return the manifest
|
||||
* @throws IOException if the manifest cannot be read
|
||||
*/
|
||||
public abstract Manifest getManifest() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns all entries from the archive.
|
||||
* @return the archive entries
|
||||
*/
|
||||
public abstract Collection<Entry> getEntries();
|
||||
Manifest getManifest() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns nested {@link Archive}s for entries that match the specified filter.
|
||||
|
@ -89,22 +52,12 @@ public abstract class Archive {
|
|||
* @return nested archives
|
||||
* @throws IOException if nested archives cannot be read
|
||||
*/
|
||||
public abstract List<Archive> getNestedArchives(EntryFilter filter)
|
||||
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;
|
||||
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
|
||||
|
||||
/**
|
||||
* Represents a single entry in the archive.
|
||||
*/
|
||||
public interface Entry {
|
||||
interface Entry {
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the entry represents a directory.
|
||||
|
@ -116,14 +69,14 @@ public abstract class Archive {
|
|||
* Returns the name of the entry.
|
||||
* @return the name of the entry
|
||||
*/
|
||||
AsciiBytes getName();
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface to filter {@link Entry Entries}.
|
||||
*/
|
||||
public interface EntryFilter {
|
||||
interface EntryFilter {
|
||||
|
||||
/**
|
||||
* 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.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by an exploded archive directory.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ExplodedArchive extends Archive {
|
||||
public class ExplodedArchive implements Archive {
|
||||
|
||||
private static final Set<String> SKIPPED_NAMES = new HashSet<String>(
|
||||
Arrays.asList(".", ".."));
|
||||
|
||||
private static final AsciiBytes MANIFEST_ENTRY_NAME = new AsciiBytes(
|
||||
"META-INF/MANIFEST.MF");
|
||||
|
||||
private final File root;
|
||||
|
||||
private Map<AsciiBytes, Entry> entries = new LinkedHashMap<AsciiBytes, Entry>();
|
||||
private final boolean recursive;
|
||||
|
||||
private File manifestFile;
|
||||
|
||||
private Manifest manifest;
|
||||
|
||||
private boolean filtered = false;
|
||||
|
||||
/**
|
||||
* Create a new {@link ExplodedArchive} instance.
|
||||
* @param root the root folder
|
||||
|
@ -78,52 +71,24 @@ public class ExplodedArchive extends Archive {
|
|||
throw new IllegalArgumentException("Invalid source folder " + root);
|
||||
}
|
||||
this.root = root;
|
||||
buildEntries(root, recursive);
|
||||
this.entries = Collections.unmodifiableMap(this.entries);
|
||||
this.recursive = recursive;
|
||||
this.manifestFile = getManifestFile(root);
|
||||
}
|
||||
|
||||
private ExplodedArchive(File root, Map<AsciiBytes, Entry> entries) {
|
||||
this.root = root;
|
||||
// The entries are pre-filtered
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private File getManifestFile(File root) {
|
||||
File metaInf = new File(root, "META-INF");
|
||||
return new File(metaInf, "MANIFEST.MF");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
FilteredURLStreamHandler handler = this.filtered ? new FilteredURLStreamHandler()
|
||||
: null;
|
||||
return new URL("file", "", -1, this.root.toURI().getPath(), handler);
|
||||
return new URL("file", "", -1, this.root.toURI().getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
if (this.manifest == null && this.entries.containsKey(MANIFEST_ENTRY_NAME)) {
|
||||
FileEntry entry = (FileEntry) this.entries.get(MANIFEST_ENTRY_NAME);
|
||||
FileInputStream inputStream = new FileInputStream(entry.getFile());
|
||||
if (this.manifest == null && this.manifestFile.exists()) {
|
||||
FileInputStream inputStream = new FileInputStream(this.manifestFile);
|
||||
try {
|
||||
this.manifest = new Manifest(inputStream);
|
||||
}
|
||||
|
@ -137,7 +102,7 @@ public class ExplodedArchive extends Archive {
|
|||
@Override
|
||||
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
||||
List<Archive> nestedArchives = new ArrayList<Archive>();
|
||||
for (Entry entry : getEntries()) {
|
||||
for (Entry entry : this) {
|
||||
if (filter.matches(entry)) {
|
||||
nestedArchives.add(getNestedArchive(entry));
|
||||
}
|
||||
|
@ -146,8 +111,8 @@ public class ExplodedArchive extends Archive {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Collection<Entry> getEntries() {
|
||||
return Collections.unmodifiableCollection(this.entries.values());
|
||||
public Iterator<Entry> iterator() {
|
||||
return new FileEntryIterator(this.root, this.recursive);
|
||||
}
|
||||
|
||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||
|
@ -157,25 +122,94 @@ public class ExplodedArchive extends Archive {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException {
|
||||
Map<AsciiBytes, Entry> filteredEntries = new LinkedHashMap<AsciiBytes, Archive.Entry>();
|
||||
for (Map.Entry<AsciiBytes, Entry> entry : this.entries.entrySet()) {
|
||||
AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue());
|
||||
if (filteredName != null) {
|
||||
filteredEntries.put(filteredName, new FileEntry(filteredName,
|
||||
((FileEntry) entry.getValue()).getFile()));
|
||||
}
|
||||
public String toString() {
|
||||
try {
|
||||
return getUrl().toString();
|
||||
}
|
||||
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;
|
||||
|
||||
FileEntry(AsciiBytes name, File file) {
|
||||
FileEntry(String name, File file) {
|
||||
this.name = name;
|
||||
this.file = file;
|
||||
}
|
||||
|
@ -190,43 +224,9 @@ public class ExplodedArchive extends Archive {
|
|||
}
|
||||
|
||||
@Override
|
||||
public AsciiBytes getName() {
|
||||
public String getName() {
|
||||
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.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
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.util.AsciiBytes;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by a {@link JarFile}.
|
||||
|
@ -43,16 +41,14 @@ import org.springframework.boot.loader.util.AsciiBytes;
|
|||
* @author Phillip Webb
|
||||
* @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 final JarFile jarFile;
|
||||
|
||||
private final List<Entry> entries;
|
||||
|
||||
private URL url;
|
||||
|
||||
private File tempUnpackFolder;
|
||||
|
@ -68,11 +64,6 @@ public class JarFileArchive extends Archive {
|
|||
|
||||
public JarFileArchive(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
|
||||
|
@ -91,7 +82,7 @@ public class JarFileArchive extends Archive {
|
|||
@Override
|
||||
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
|
||||
List<Archive> nestedArchives = new ArrayList<Archive>();
|
||||
for (Entry entry : getEntries()) {
|
||||
for (Entry entry : this) {
|
||||
if (filter.matches(entry)) {
|
||||
nestedArchives.add(getNestedArchive(entry));
|
||||
}
|
||||
|
@ -100,27 +91,27 @@ public class JarFileArchive extends Archive {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Collection<Entry> getEntries() {
|
||||
return Collections.unmodifiableCollection(this.entries);
|
||||
public Iterator<Entry> iterator() {
|
||||
return new EntryIterator(this.jarFile.entries());
|
||||
}
|
||||
|
||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||
JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
|
||||
if (data.getComment().startsWith(UNPACK_MARKER)) {
|
||||
return getUnpackedNestedArchive(data);
|
||||
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
|
||||
if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
|
||||
return getUnpackedNestedArchive(jarEntry);
|
||||
}
|
||||
JarFile jarFile = this.jarFile.getNestedJarFile(data);
|
||||
JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
|
||||
return new JarFileArchive(jarFile);
|
||||
}
|
||||
|
||||
private Archive getUnpackedNestedArchive(JarEntryData data) throws IOException {
|
||||
String name = data.getName().toString();
|
||||
private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
|
||||
String name = jarEntry.getName();
|
||||
if (name.lastIndexOf("/") != -1) {
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
}
|
||||
File file = new File(getTempUnpackFolder(), name);
|
||||
if (!file.exists() || file.length() != data.getSize()) {
|
||||
unpack(data, file);
|
||||
if (!file.exists() || file.length() != jarEntry.getSize()) {
|
||||
unpack(jarEntry, file);
|
||||
}
|
||||
return new JarFileArchive(file, file.toURI().toURL());
|
||||
}
|
||||
|
@ -147,8 +138,8 @@ public class JarFileArchive extends Archive {
|
|||
"Failed to create unpack folder in directory '" + parent + "'");
|
||||
}
|
||||
|
||||
private void unpack(JarEntryData data, File file) throws IOException {
|
||||
InputStream inputStream = data.getData().getInputStream(ResourceAccess.ONCE);
|
||||
private void unpack(JarEntry entry, File file) throws IOException {
|
||||
InputStream inputStream = this.jarFile.getInputStream(entry, ResourceAccess.ONCE);
|
||||
try {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
try {
|
||||
|
@ -169,14 +160,41 @@ public class JarFileArchive extends Archive {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
|
||||
JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
|
||||
@Override
|
||||
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
||||
return filter.apply(name, new JarFileEntry(entryData));
|
||||
}
|
||||
});
|
||||
return new JarFileArchive(filteredJar);
|
||||
public String toString() {
|
||||
try {
|
||||
return getUrl().toString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
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 final JarEntryData entryData;
|
||||
private final JarEntry jarEntry;
|
||||
|
||||
JarFileEntry(JarEntryData entryData) {
|
||||
this.entryData = entryData;
|
||||
JarFileEntry(JarEntry jarEntry) {
|
||||
this.jarEntry = jarEntry;
|
||||
}
|
||||
|
||||
public JarEntryData getJarEntryData() {
|
||||
return this.entryData;
|
||||
public JarEntry getJarEntry() {
|
||||
return this.jarEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.entryData.isDirectory();
|
||||
return this.jarEntry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsciiBytes getName() {
|
||||
return this.entryData.getName();
|
||||
public String getName() {
|
||||
return this.jarEntry.getName().toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.util;
|
||||
package org.springframework.boot.loader.jar;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
|
@ -24,14 +24,10 @@ import java.nio.charset.Charset;
|
|||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public final class AsciiBytes {
|
||||
final class AsciiBytes {
|
||||
|
||||
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 int offset;
|
||||
|
@ -40,11 +36,13 @@ public final class AsciiBytes {
|
|||
|
||||
private String string;
|
||||
|
||||
private int hash;
|
||||
|
||||
/**
|
||||
* Create a new {@link AsciiBytes} from the specified String.
|
||||
* @param string the source string
|
||||
*/
|
||||
public AsciiBytes(String string) {
|
||||
AsciiBytes(String string) {
|
||||
this(string.getBytes(UTF_8));
|
||||
this.string = string;
|
||||
}
|
||||
|
@ -54,7 +52,7 @@ public final class AsciiBytes {
|
|||
* are not expected to change.
|
||||
* @param bytes the source bytes
|
||||
*/
|
||||
public AsciiBytes(byte[] bytes) {
|
||||
AsciiBytes(byte[] bytes) {
|
||||
this(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
|
@ -65,7 +63,7 @@ public final class AsciiBytes {
|
|||
* @param offset the offset
|
||||
* @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) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
@ -155,12 +153,28 @@ public final class AsciiBytes {
|
|||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = INITIAL_HASH;
|
||||
for (int i = 0; i < this.length; i++) {
|
||||
hash = MULTIPLIER * hash + this.bytes[this.offset + i];
|
||||
int hash = this.hash;
|
||||
if (hash == 0 && this.bytes.length > 0) {
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -185,4 +199,17 @@ public final class AsciiBytes {
|
|||
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.InputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
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
|
||||
* the entry is actually needed.
|
||||
* A ZIP File "Central directory file header record" (CDFH).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @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 final JarFile source;
|
||||
|
||||
private final byte[] header;
|
||||
|
||||
private AsciiBytes name;
|
||||
|
@ -52,15 +46,8 @@ public final class JarEntryData {
|
|||
|
||||
private final long localHeaderOffset;
|
||||
|
||||
private RandomAccessData data;
|
||||
|
||||
private SoftReference<JarEntry> entry;
|
||||
|
||||
JarFile nestedJar;
|
||||
|
||||
public JarEntryData(JarFile source, byte[] header, InputStream inputStream)
|
||||
CentralDirectoryFileHeader(byte[] header, InputStream inputStream)
|
||||
throws IOException {
|
||||
this.source = source;
|
||||
this.header = header;
|
||||
long nameLength = Bytes.littleEndianValue(header, 28, 2);
|
||||
long extraLength = Bytes.littleEndianValue(header, 30, 2);
|
||||
|
@ -71,79 +58,20 @@ public final class JarEntryData {
|
|||
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() {
|
||||
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() {
|
||||
return this.name.endsWith(SLASH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMethod() {
|
||||
return (int) Bytes.littleEndianValue(this.header, 10, 2);
|
||||
}
|
||||
|
@ -176,12 +104,14 @@ public final class JarEntryData {
|
|||
return Bytes.littleEndianValue(this.header, 16, 4);
|
||||
}
|
||||
|
||||
public int getCompressedSize() {
|
||||
return (int) Bytes.littleEndianValue(this.header, 20, 4);
|
||||
@Override
|
||||
public long getCompressedSize() {
|
||||
return Bytes.littleEndianValue(this.header, 20, 4);
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return (int) Bytes.littleEndianValue(this.header, 24, 4);
|
||||
@Override
|
||||
public long getSize() {
|
||||
return Bytes.littleEndianValue(this.header, 24, 4);
|
||||
}
|
||||
|
||||
public byte[] getExtra() {
|
||||
|
@ -192,24 +122,37 @@ public final class JarEntryData {
|
|||
return this.comment;
|
||||
}
|
||||
|
||||
JarEntryData createFilteredCopy(JarFile jarFile, AsciiBytes name) {
|
||||
return new JarEntryData(this, jarFile, name);
|
||||
@Override
|
||||
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.
|
||||
* @param source the source {@link JarFile}
|
||||
* Create a new {@link CentralDirectoryFileHeader} instance from the specified input
|
||||
* stream.
|
||||
* @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
|
||||
*/
|
||||
static JarEntryData fromInputStream(JarFile source, InputStream inputStream)
|
||||
static CentralDirectoryFileHeader fromInputStream(InputStream inputStream)
|
||||
throws IOException {
|
||||
byte[] header = new byte[46];
|
||||
if (!Bytes.fill(inputStream, header)) {
|
||||
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
|
||||
*/
|
||||
public class JarEntry extends java.util.jar.JarEntry {
|
||||
|
||||
private final JarEntryData source;
|
||||
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
||||
|
||||
private Certificate[] certificates;
|
||||
|
||||
private CodeSigner[] codeSigners;
|
||||
|
||||
public JarEntry(JarEntryData source) {
|
||||
super(source.getName().toString());
|
||||
this.source = source;
|
||||
private final JarFile jarFile;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the source {@link JarEntryData} that was used to create this entry.
|
||||
* @return the source of the entry
|
||||
*/
|
||||
public JarEntryData getSource() {
|
||||
return this.source;
|
||||
@Override
|
||||
public boolean hasName(String name, String suffix) {
|
||||
return getName().length() == name.length() + suffix.length()
|
||||
&& getName().startsWith(name) && getName().endsWith(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,35 +64,40 @@ public class JarEntry extends java.util.jar.JarEntry {
|
|||
* @return the URL for the entry
|
||||
* @throws MalformedURLException if the URL is not valid
|
||||
*/
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
return new URL(this.source.getSource().getUrl(), getName());
|
||||
URL getUrl() throws MalformedURLException {
|
||||
return new URL(this.jarFile.getUrl(), getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() throws IOException {
|
||||
Manifest manifest = this.source.getSource().getManifest();
|
||||
Manifest manifest = this.jarFile.getManifest();
|
||||
return (manifest == null ? null : manifest.getAttributes(getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Certificate[] getCertificates() {
|
||||
if (this.source.getSource().isSigned() && this.certificates == null) {
|
||||
this.source.getSource().setupEntryCertificates();
|
||||
if (this.jarFile.isSigned() && this.certificates == null) {
|
||||
this.jarFile.setupEntryCertificates(this);
|
||||
}
|
||||
return this.certificates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeSigner[] getCodeSigners() {
|
||||
if (this.source.getSource().isSigned() && this.codeSigners == null) {
|
||||
this.source.getSource().setupEntryCertificates();
|
||||
if (this.jarFile.isSigned() && this.codeSigners == null) {
|
||||
this.jarFile.setupEntryCertificates(this);
|
||||
}
|
||||
return this.codeSigners;
|
||||
}
|
||||
|
||||
void setupCertificates(java.util.jar.JarEntry entry) {
|
||||
void setCertificates(java.util.jar.JarEntry entry) {
|
||||
this.certificates = entry.getCertificates();
|
||||
this.codeSigners = entry.getCodeSigners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLocalHeaderOffset() {
|
||||
return this.localHeaderOffset;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,23 +16,20 @@
|
|||
|
||||
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.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public interface JarEntryFilter {
|
||||
interface JarEntryFilter {
|
||||
|
||||
/**
|
||||
* Apply the jar entry filter.
|
||||
* @param name the current entry name. This may be different that the original entry
|
||||
* 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
|
||||
* 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.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.jar.Manifest;
|
||||
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.ResourceAccess;
|
||||
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
|
||||
* offers the following additional functionality.
|
||||
* <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
|
||||
* on any directory entry.</li>
|
||||
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
|
||||
* embedded JAR files (as long as their entry is not compressed).</li>
|
||||
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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 AsciiBytes MANIFEST_MF = new AsciiBytes("META-INF/MANIFEST.MF");
|
||||
|
||||
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
||||
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
|
||||
|
||||
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
|
||||
|
||||
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;
|
||||
|
||||
|
@ -74,17 +64,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
|
||||
private final RandomAccessData data;
|
||||
|
||||
private final List<JarEntryData> entries;
|
||||
private URL url;
|
||||
|
||||
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
|
||||
|
||||
private boolean signed;
|
||||
|
||||
private JarEntryData manifestEntry;
|
||||
private JarFileEntries entries;
|
||||
|
||||
private SoftReference<Manifest> manifest;
|
||||
|
||||
private URL url;
|
||||
private boolean signed;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
RandomAccessData data) throws IOException {
|
||||
super(rootFile.getFile());
|
||||
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
|
||||
this.rootFile = rootFile;
|
||||
this.pathFromRoot = pathFromRoot;
|
||||
this.data = getArchiveData(endRecord, data);
|
||||
this.entries = loadJarEntries(endRecord);
|
||||
this(rootFile, pathFromRoot, data, null);
|
||||
}
|
||||
|
||||
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
|
||||
RandomAccessData data, List<JarEntryData> entries, JarEntryFilter... filters)
|
||||
throws IOException {
|
||||
RandomAccessData data, JarEntryFilter filter) throws IOException {
|
||||
super(rootFile.getFile());
|
||||
this.rootFile = rootFile;
|
||||
this.pathFromRoot = pathFromRoot;
|
||||
this.data = data;
|
||||
this.entries = filterEntries(entries, filters);
|
||||
CentralDirectoryParser parser = new CentralDirectoryParser();
|
||||
this.entries = parser.addVistor(new JarFileEntries(this, filter));
|
||||
parser.addVistor(centralDirectoryVistor());
|
||||
this.data = parser.parse(data, filter == null);
|
||||
}
|
||||
|
||||
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 CentralDirectoryVistor centralDirectoryVistor() {
|
||||
return new CentralDirectoryVistor() {
|
||||
|
||||
private List<JarEntryData> loadJarEntries(CentralDirectoryEndRecord endRecord)
|
||||
throws IOException {
|
||||
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
|
||||
int numberOfRecords = endRecord.getNumberOfRecords();
|
||||
List<JarEntryData> entries = new ArrayList<JarEntryData>(numberOfRecords);
|
||||
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
|
||||
try {
|
||||
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
|
||||
while (entry != null) {
|
||||
entries.add(entry);
|
||||
processEntry(entry);
|
||||
entry = JarEntryData.fromInputStream(this, inputStream);
|
||||
@Override
|
||||
public void visitStart(CentralDirectoryEndRecord endRecord,
|
||||
RandomAccessData centralDirectoryData) {
|
||||
}
|
||||
}
|
||||
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));
|
||||
@Override
|
||||
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
|
||||
int dataOffset) {
|
||||
AsciiBytes name = fileHeader.getName();
|
||||
if (name.startsWith(META_INF)
|
||||
&& name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
||||
JarFile.this.signed = true;
|
||||
}
|
||||
}
|
||||
if (name != null) {
|
||||
JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
|
||||
filteredEntries.add(filteredCopy);
|
||||
processEntry(filteredCopy);
|
||||
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (name.equals(MANIFEST_MF)) {
|
||||
this.manifestEntry = entry;
|
||||
}
|
||||
if (name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
||||
this.signed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected final RandomAccessDataFile getRootJarFile() {
|
||||
|
@ -205,12 +149,12 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
if (this.manifestEntry == null) {
|
||||
return null;
|
||||
}
|
||||
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
|
||||
if (manifest == null) {
|
||||
InputStream inputStream = this.manifestEntry.getInputStream();
|
||||
InputStream inputStream = getInputStream(MANIFEST_NAME, ResourceAccess.ONCE);
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
manifest = new Manifest(inputStream);
|
||||
}
|
||||
|
@ -224,7 +168,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
|
||||
@Override
|
||||
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>() {
|
||||
|
||||
@Override
|
||||
|
@ -234,14 +178,10 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
|
||||
@Override
|
||||
public java.util.jar.JarEntry nextElement() {
|
||||
return iterator.next().asJarEntry();
|
||||
return iterator.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<JarEntryData> iterator() {
|
||||
return this.entries.iterator();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -251,72 +191,24 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
|
||||
@Override
|
||||
public ZipEntry getEntry(String name) {
|
||||
JarEntryData jarEntryData = getJarEntryData(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);
|
||||
}
|
||||
return this.entries.getEntry(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
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)
|
||||
throws IOException {
|
||||
return getNestedJarFile(getContainedEntry(entry).getSource());
|
||||
return getNestedJarFile((JarEntry) 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
|
||||
* @throws IOException if the nested jar file cannot be read
|
||||
*/
|
||||
public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
|
||||
try {
|
||||
if (sourceEntry.nestedJar == null) {
|
||||
sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
|
||||
}
|
||||
return sourceEntry.nestedJar;
|
||||
return createJarFileFromEntry(entry);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
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 {
|
||||
if (sourceEntry.isDirectory()) {
|
||||
return createJarFileFromDirectoryEntry(sourceEntry);
|
||||
private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
|
||||
if (entry.isDirectory()) {
|
||||
return createJarFileFromDirectoryEntry(entry);
|
||||
}
|
||||
return createJarFileFromFileEntry(sourceEntry);
|
||||
return createJarFileFromFileEntry(entry);
|
||||
}
|
||||
|
||||
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
final AsciiBytes sourceName = sourceEntry.getName();
|
||||
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
|
||||
final AsciiBytes sourceName = new AsciiBytes(entry.getName());
|
||||
JarEntryFilter filter = new JarEntryFilter() {
|
||||
@Override
|
||||
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
||||
public AsciiBytes apply(AsciiBytes name) {
|
||||
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
||||
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,
|
||||
this.pathFromRoot + "!/"
|
||||
+ sourceEntry.getName().substring(0, sourceName.length() - 1),
|
||||
this.data, this.entries, filter);
|
||||
+ entry.getName().substring(0, sourceName.length() - 1),
|
||||
this.data, filter);
|
||||
}
|
||||
|
||||
private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
|
||||
throws IOException {
|
||||
if (sourceEntry.getMethod() != ZipEntry.STORED) {
|
||||
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
|
||||
if (entry.getMethod() != ZipEntry.STORED) {
|
||||
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 "
|
||||
+ "mechanism used to create your executable jar file");
|
||||
}
|
||||
return new JarFile(this.rootFile,
|
||||
this.pathFromRoot + "!/" + sourceEntry.getName(), sourceEntry.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
|
||||
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
|
||||
entryData);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -444,6 +311,42 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
|
|||
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
|
||||
* {@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.URLConnection;
|
||||
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()}.
|
||||
|
@ -63,11 +60,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private JarEntryData jarEntryData;
|
||||
|
||||
private URL jarFileUrl;
|
||||
|
||||
private JarEntryName jarEntryName;
|
||||
private final JarEntryName jarEntryName;
|
||||
|
||||
private JarEntry jarEntry;
|
||||
|
||||
protected JarURLConnection(URL url, JarFile jarFile) throws IOException {
|
||||
// What we pass to super is ultimately ignored
|
||||
|
@ -100,10 +97,9 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (!this.jarEntryName.isEmpty()) {
|
||||
this.jarEntryData = this.jarFile
|
||||
.getJarEntryData(this.jarEntryName.asAsciiBytes());
|
||||
if (this.jarEntryData == null) {
|
||||
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
|
||||
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
|
||||
if (this.jarEntry == null) {
|
||||
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||
}
|
||||
}
|
||||
|
@ -119,16 +115,6 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
"JAR entry " + entry + " not found in " + jarFile.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
try {
|
||||
return super.getManifest();
|
||||
}
|
||||
finally {
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarFile getJarFile() throws IOException {
|
||||
connect();
|
||||
|
@ -161,8 +147,11 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
@Override
|
||||
public JarEntry getJarEntry() throws IOException {
|
||||
if (this.jarEntryName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
connect();
|
||||
return (this.jarEntryData == null ? null : this.jarEntryData.asJarEntry());
|
||||
return this.jarEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -172,21 +161,25 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
connect();
|
||||
if (this.jarEntryName.isEmpty()) {
|
||||
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
|
||||
public int getContentLength() {
|
||||
try {
|
||||
connect();
|
||||
if (this.jarEntryData != null) {
|
||||
return this.jarEntryData.getSize();
|
||||
if (this.jarEntryName.isEmpty()) {
|
||||
return this.jarFile.size();
|
||||
}
|
||||
return this.jarFile.size();
|
||||
JarEntry entry = getJarEntry();
|
||||
return (entry == null ? -1 : (int) entry.getSize());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return -1;
|
||||
|
@ -196,7 +189,7 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
@Override
|
||||
public Object getContent() throws IOException {
|
||||
connect();
|
||||
return (this.jarEntryData == null ? this.jarFile : super.getContent());
|
||||
return (this.jarEntryName.isEmpty() ? this.jarFile : super.getContent());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -213,7 +206,7 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
*/
|
||||
private static class JarEntryName {
|
||||
|
||||
private final AsciiBytes name;
|
||||
private final String name;
|
||||
|
||||
private String contentType;
|
||||
|
||||
|
@ -221,26 +214,26 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
this.name = decode(spec);
|
||||
}
|
||||
|
||||
private AsciiBytes decode(String source) {
|
||||
private String decode(String source) {
|
||||
int length = (source == null ? 0 : source.length());
|
||||
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++) {
|
||||
int ch = source.charAt(i);
|
||||
if (ch == '%') {
|
||||
int c = source.charAt(i);
|
||||
if (c == '%') {
|
||||
if ((i + 2) >= length) {
|
||||
throw new IllegalArgumentException("Invalid encoded sequence \""
|
||||
+ source.substring(i) + "\"");
|
||||
}
|
||||
ch = decodeEscapeSequence(source, i);
|
||||
c = decodeEscapeSequence(source, i);
|
||||
i += 2;
|
||||
}
|
||||
bos.write(ch);
|
||||
outputStream.write(c);
|
||||
}
|
||||
// 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) {
|
||||
|
@ -258,10 +251,6 @@ class JarURLConnection extends java.net.JarURLConnection {
|
|||
return this.name.toString();
|
||||
}
|
||||
|
||||
public AsciiBytes asAsciiBytes() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
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.archive.Archive.Entry;
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
|
@ -130,27 +128,6 @@ public class ExplodedArchiveTests {
|
|||
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
|
||||
public void getNonRecursiveEntriesForRoot() throws Exception {
|
||||
ExplodedArchive archive = new ExplodedArchive(new File("/"), false);
|
||||
|
@ -198,7 +175,7 @@ public class ExplodedArchiveTests {
|
|||
|
||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||
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);
|
||||
}
|
||||
return entries;
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.junit.rules.TemporaryFolder;
|
|||
|
||||
import org.springframework.boot.loader.TestJarCreator;
|
||||
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.equalTo;
|
||||
|
@ -126,25 +125,9 @@ public class JarFileArchiveTests {
|
|||
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) {
|
||||
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);
|
||||
}
|
||||
return entries;
|
||||
|
|
|
@ -14,14 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader;
|
||||
package org.springframework.boot.loader.jar;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
@ -138,8 +136,22 @@ public class AsciiBytesTests {
|
|||
assertThat(bc, equalTo(bc));
|
||||
assertThat(bc, equalTo(bc_substring));
|
||||
assertThat(bc, equalTo(bc_string));
|
||||
|
||||
assertThat(bc.hashCode(), not(equalTo(abcd.hashCode())));
|
||||
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.data.RandomAccessDataFile;
|
||||
import org.springframework.boot.loader.util.AsciiBytes;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
|
@ -349,27 +348,6 @@ public class JarFileTests {
|
|||
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
|
||||
public void sensibleToString() throws Exception {
|
||||
assertThat(this.jarFile.toString(), equalTo(this.rootJarFile.getPath()));
|
||||
|
|
Loading…
Reference in New Issue