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
pull/5060/head
Phillip Webb 9 years ago
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 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 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 entry.getName().startsWith(this.prefix) && this.filter.matches(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,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<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 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<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,75 +122,110 @@ 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 File file;
private final boolean recursive;
FileEntry(AsciiBytes name, File file) {
this.name = name;
this.file = file;
}
private final Deque<Iterator<File>> stack = new LinkedList<Iterator<File>>();
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<File> listFiles(File file) {
File[] files = file.listFiles();
if (files == null) {
return Collections.<File>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;
}
}
return new FileNotFoundURLConnection(url, name);
this.stack.poll();
}
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;
}
}

@ -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() {
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 AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
return filter.apply(name, new JarFileEntry(entryData));
public void remove() {
throw new UnsupportedOperationException("remove");
}
});
return new JarFileArchive(filteredJar);
}
/**
@ -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 SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
private boolean signed;
private URL url;
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);
}
}
finally {
inputStream.close();
}
return entries;
@Override
public void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
}
private List<JarEntryData> filterEntries(List<JarEntryData> entries,
JarEntryFilter[] filters) {
List<JarEntryData> filteredEntries = new ArrayList<JarEntryData>(entries.size());
for (JarEntryData entry : entries) {
AsciiBytes name = entry.getName();
for (JarEntryFilter filter : filters) {
name = (filter == null || name == null ? name
: filter.apply(name, entry));
}
if (name != null) {
JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
filteredEntries.add(filteredCopy);
processEntry(filteredCopy);
}
@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<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());
return this.entries.getEntry(name);
}
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;
@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<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,22 +161,26 @@ 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();
}
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…
Cancel
Save