From e2368b909b46bc5bcec6792fb208ba9bd0fe6aaa Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 11 Jan 2016 10:17:19 +0000 Subject: [PATCH] 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 --- .../loader/ExecutableArchiveLauncher.java | 12 +- .../boot/loader/JarLauncher.java | 3 +- .../boot/loader/LaunchedURLClassLoader.java | 7 +- .../boot/loader/PropertiesLauncher.java | 87 +++-- .../boot/loader/WarLauncher.java | 10 +- .../boot/loader/archive/Archive.java | 78 +---- .../boot/loader/archive/ExplodedArchive.java | 188 +++++------ .../boot/loader/archive/FilteredArchive.java | 94 ------ .../boot/loader/archive/JarFileArchive.java | 102 +++--- .../boot/loader/{util => jar}/AsciiBytes.java | 53 ++- ...a.java => CentralDirectoryFileHeader.java} | 129 ++------ .../loader/jar/CentralDirectoryParser.java | 107 ++++++ .../loader/jar/CentralDirectoryVistor.java | 35 ++ .../boot/loader/jar/FileHeader.java | 64 ++++ .../boot/loader/jar/JarEntry.java | 54 +-- .../boot/loader/jar/JarEntryFilter.java | 7 +- .../boot/loader/jar/JarFile.java | 307 ++++++------------ .../boot/loader/jar/JarFileEntries.java | 301 +++++++++++++++++ .../boot/loader/jar/JarURLConnection.java | 71 ++-- .../loader/archive/ExplodedArchiveTests.java | 25 +- .../loader/archive/JarFileArchiveTests.java | 19 +- .../loader/{ => jar}/AsciiBytesTests.java | 20 +- .../jar/CentralDirectoryParserTests.java | 122 +++++++ .../boot/loader/jar/JarFileTests.java | 22 -- 24 files changed, 1132 insertions(+), 785 deletions(-) delete mode 100644 spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java rename spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{util => jar}/AsciiBytes.java (80%) rename spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/{JarEntryData.java => CentralDirectoryFileHeader.java} (50%) create mode 100644 spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java create mode 100644 spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVistor.java create mode 100644 spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java create mode 100644 spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java rename spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => jar}/AsciiBytesTests.java (92%) create mode 100644 spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java index 89f59bd1f3..6ecb93d360 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -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 diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index 4f1f9b50ed..d004d0175f 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -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() { } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index 533d67665d..b8e7a27246 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -204,20 +204,15 @@ public class LaunchedURLClassLoader extends URLClassLoader { AccessController.doPrivileged(new PrivilegedExceptionAction() { @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) { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index 5fa4ad6cd5..59b810cfbe 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -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 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 paths = new ArrayList(DEFAULT_PATHS); + private List paths = new ArrayList(); 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 loaderClass = (Class) 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 that have a prefix in their file path + * (e.g. "lib/"). + */ + private static final class PrefixMatchingArchiveFilter implements EntryFilter { + + private final String prefix; + + private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); + + private PrefixMatchingArchiveFilter(String 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 AsciiBytes DOT_JAR = new AsciiBytes(".jar"); + private static final String DOT_JAR = ".jar"; - private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip"); + private static final String DOT_ZIP = ".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/"). + * Decorator to apply an {@link Archive.EntryFilter} to an existing {@link Archive}. */ - private static final class PrefixMatchingArchiveFilter implements EntryFilter { + private static class FilteredArchive implements Archive { - private final AsciiBytes prefix; + private final Archive parent; - private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); + private final EntryFilter filter; - private PrefixMatchingArchiveFilter(String prefix) { - this.prefix = new AsciiBytes(prefix); + FilteredArchive(Archive parent, EntryFilter filter) { + this.parent = parent; + this.filter = filter; } @Override - public boolean matches(Entry entry) { - return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); + public URL getUrl() throws MalformedURLException { + return this.parent.getUrl(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.parent.getManifest(); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public List 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); + } + }); } - } + } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java index 4aad3aaf80..7abbc8537f 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -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(); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java index dd259497f5..701b6c908a 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -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 { /** * 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 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 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 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); - - } - } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java index 421da2ddcc..d96f4198f1 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -18,45 +18,38 @@ 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 SKIPPED_NAMES = new HashSet( Arrays.asList(".", "..")); - private static final AsciiBytes MANIFEST_ENTRY_NAME = new AsciiBytes( - "META-INF/MANIFEST.MF"); - private final File root; - private Map entries = new LinkedHashMap(); + private final boolean recursive; - private Manifest manifest; + private File manifestFile; - private boolean filtered = false; + private Manifest manifest; /** * Create a new {@link ExplodedArchive} instance. @@ -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 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 getNestedArchives(EntryFilter filter) throws IOException { List nestedArchives = new ArrayList(); - 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 getEntries() { - return Collections.unmodifiableCollection(this.entries.values()); + public Iterator iterator() { + return new FileEntryIterator(this.root, this.recursive); } protected Archive getNestedArchive(Entry entry) throws IOException { @@ -157,75 +122,110 @@ public class ExplodedArchive extends Archive { } @Override - public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException { - Map filteredEntries = new LinkedHashMap(); - for (Map.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 { - private final AsciiBytes name; + private final File root; - private final File file; + private final boolean recursive; - FileEntry(AsciiBytes name, File file) { - this.name = name; - this.file = file; - } + private final Deque> stack = new LinkedList>(); - public File getFile() { - return this.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 isDirectory() { - return this.file.isDirectory(); + public boolean hasNext() { + return this.current != null; } @Override - public AsciiBytes getName() { - return this.name; + 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); } - } - /** - * {@link URLStreamHandler} that respects filtered entries. - */ - private class FilteredURLStreamHandler extends URLStreamHandler { + private Iterator listFiles(File file) { + File[] files = file.listFiles(); + if (files == null) { + return Collections.emptyList().iterator(); + } + return Arrays.asList(files).iterator(); + } - @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(); + 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 new FileNotFoundURLConnection(url, name); + return null; } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + } /** - * {@link URLConnection} used to represent a filtered file. + * {@link Entry} backed by a File. */ - private static class FileNotFoundURLConnection extends URLConnection { + private static class FileEntry implements Entry { private final String name; - FileNotFoundURLConnection(URL url, String name) { - super(url); + private final File file; + + FileEntry(String name, File file) { this.name = name; + this.file = file; + } + + public File getFile() { + return this.file; } @Override - public void connect() throws IOException { - throw new FileNotFoundException(this.name); + public boolean isDirectory() { + return this.file.isDirectory(); + } + + @Override + public String getName() { + return this.name; } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java deleted file mode 100644 index 1289fca026..0000000000 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java +++ /dev/null @@ -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 getEntries() { - List nested = new ArrayList(); - for (Entry entry : this.parent.getEntries()) { - if (this.filter.matches(entry)) { - nested.add(entry); - } - } - return Collections.unmodifiableList(nested); - } - - @Override - public List 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; - } - }); - } - -} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index f34f4a8edb..1a39f2efc9 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -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 entries; - private URL url; private File tempUnpackFolder; @@ -68,11 +64,6 @@ public class JarFileArchive extends Archive { public JarFileArchive(JarFile jarFile) { this.jarFile = jarFile; - ArrayList jarFileEntries = new ArrayList(); - 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 getNestedArchives(EntryFilter filter) throws IOException { List nestedArchives = new ArrayList(); - 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 getEntries() { - return Collections.unmodifiableCollection(this.entries); + public Iterator 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 { + + private final Enumeration enumeration; + + EntryIterator(Enumeration 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(); } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java similarity index 80% rename from spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java rename to spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java index 1b44675bf9..52bc3983bb 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/AsciiBytes.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -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; + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java similarity index 50% rename from spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java rename to spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java index b8ad24551c..e0148776c4 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -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 Zip File Format */ -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 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(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); } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java new file mode 100644 index 0000000000..dc2562da7c --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -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 vistors = new ArrayList(); + + public 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(); + } + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVistor.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVistor.java new file mode 100644 index 0000000000..d7708aa338 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVistor.java @@ -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(); + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java new file mode 100644 index 0000000000..29784014a7 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -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(); + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 23a078b119..de68915b01 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -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; + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java index a7ff5545f6..be5b03b671 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -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); } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index ca66f6773b..b9a23927a6 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -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. *
    - *
  • New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} - * from existing files.
  • *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based * on any directory entry.
  • *
  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for * embedded JAR files (as long as their entry is not compressed).
  • - *
  • Entry data can be accessed as {@link RandomAccessData}.
  • *
* * @author Phillip Webb */ -public class JarFile extends java.util.jar.JarFile implements Iterable { +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 entries; - - private SoftReference> entriesByName; - - private boolean signed; + private URL url; - private JarEntryData manifestEntry; + private JarFileEntries entries; private SoftReference 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 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 loadJarEntries(CentralDirectoryEndRecord endRecord) - throws IOException { - RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data); - int numberOfRecords = endRecord.getNumberOfRecords(); - List entries = new ArrayList(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 filterEntries(List entries, - JarEntryFilter[] filters) { - List filteredEntries = new ArrayList(entries.size()); - for (JarEntryData entry : entries) { - AsciiBytes name = entry.getName(); - for (JarEntryFilter filter : filters) { - name = (filter == null || name == null ? name - : filter.apply(name, entry)); - } - if (name != null) { - JarEntryData filteredCopy = entry.createFilteredCopy(this, name); - filteredEntries.add(filteredCopy); - processEntry(filteredCopy); + @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; + } } - } - return filteredEntries; - } - private void processEntry(JarEntryData entry) { - AsciiBytes name = entry.getName(); - if (name.startsWith(META_INF)) { - processMetaInfEntry(name, entry); - } - } + @Override + public void visitEnd() { + } - 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 entries() { - final Iterator iterator = iterator(); + final Iterator iterator = this.entries.iterator(); return new Enumeration() { @Override @@ -234,14 +178,10 @@ public class JarFile extends java.util.jar.JarFile implements Iterable iterator() { - return this.entries.iterator(); + }; } @Override @@ -251,72 +191,24 @@ public class JarFile extends java.util.jar.JarFile implements Iterable entriesByName = (this.entriesByName == null ? null - : this.entriesByName.get()); - if (entriesByName == null) { - entriesByName = new HashMap(); - for (JarEntryData entry : this.entries) { - entriesByName.put(entry.getName(), entry); - } - this.entriesByName = new SoftReference>( - 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; + @Override + public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { + return getInputStream(ze, ResourceAccess.PER_READ); } - 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); + 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); } - @Override - public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { - return getContainedEntry(ze).getSource().getInputStream(); + 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 + * 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 { + + 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 entriesCache = Collections + .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry 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 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 getEntry(String name, Class 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 getEntry(int hashCode, String name, String suffix, + Class 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 getEntry(int index, Class 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 { + + 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); + } + + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index d2c9a62a6d..4e8e330af4 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -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; } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java index cdad4c5132..06a9ab5a08 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -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 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 getEntriesMap(Archive archive) { Map entries = new HashMap(); - for (Archive.Entry entry : archive.getEntries()) { + for (Archive.Entry entry : archive) { entries.put(entry.getName().toString(), entry); } return entries; diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index f4416224c3..88c4b30a89 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -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 entries = getEntriesMap(filteredArchive); - assertThat(entries.size(), equalTo(1)); - } - private Map getEntriesMap(Archive archive) { Map entries = new HashMap(); - for (Archive.Entry entry : archive.getEntries()) { + for (Archive.Entry entry : archive) { entries.put(entry.getName().toString(), entry); } return entries; diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java similarity index 92% rename from spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java rename to spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java index fdff42a093..7fb75241d4 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java @@ -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())); + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java new file mode 100644 index 0000000000..c45c3c8847 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java @@ -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 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 headers = new ArrayList(); + + @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 getHeaders() { + return this.headers; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index e2b728589d..20ec637fa2 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -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 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()));