Improve startup performance for nested JARs

Refactor spring-boot-loader to work directly with low level zip data
structures, removing the need to read every byte when the application
loads.

This change was initially driven by the desire to improve tab-completion
time when working with the Spring CLI tool. Local tests show CLI
startup time improving from ~0.7 to ~0.22 seconds.

Startup times for regular Spring Boot applications are also improved,
for example, the tomcat sample application now starts 0.5 seconds
faster.
pull/123/head
Phillip Webb 11 years ago
parent 6a6159f106
commit d2678e08de

@ -12,6 +12,26 @@
<properties> <properties>
<main.basedir>${basedir}/../..</main.basedir> <main.basedir>${basedir}/../..</main.basedir>
</properties> </properties>
<dependencies>
<!-- Must never have compile/runtime time dependencies -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<!-- Used to provide a signed jar -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles> <profiles>
<profile> <profile>
<id>integration</id> <id>integration</id>
@ -45,16 +65,4 @@
</build> </build>
</profile> </profile>
</profiles> </profiles>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project> </project>

@ -0,0 +1,181 @@
/*
* Copyright 2012-2013 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;
import java.nio.charset.Charset;
/**
* Simple wrapper around a byte array that represents an ASCII. Used for performance
* reasons to save constructing Strings for ZIP data.
*
* @author Phillip Webb
*/
public 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;
private final int length;
private String string;
/**
* Create a new {@link AsciiBytes} from the specified String.
* @param string
*/
public AsciiBytes(String string) {
this(string.getBytes());
this.string = string;
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the bytes
*/
public AsciiBytes(byte[] bytes) {
this(bytes, 0, bytes.length);
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the bytes
* @param offset the offset
* @param length the length
*/
public AsciiBytes(byte[] bytes, int offset, int length) {
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
throw new IndexOutOfBoundsException();
}
this.bytes = bytes;
this.offset = offset;
this.length = length;
}
public int length() {
return this.length;
}
public boolean startsWith(AsciiBytes prefix) {
if (this == prefix) {
return true;
}
if (prefix.length > this.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) {
return false;
}
}
return true;
}
public boolean endsWith(AsciiBytes postfix) {
if (this == postfix) {
return true;
}
if (postfix.length > this.length) {
return false;
}
for (int i = 0; i < postfix.length; i++) {
if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset
+ (postfix.length - 1) - i]) {
return false;
}
}
return true;
}
public AsciiBytes substring(int beginIndex) {
return substring(beginIndex, this.length);
}
public AsciiBytes substring(int beginIndex, int endIndex) {
int length = endIndex - beginIndex;
if (this.offset + length > this.length) {
throw new IndexOutOfBoundsException();
}
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
}
public AsciiBytes append(String string) {
if (string == null || string.length() == 0) {
return this;
}
return append(string.getBytes());
}
public AsciiBytes append(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return this;
}
byte[] combined = new byte[this.length + bytes.length];
System.arraycopy(this.bytes, this.offset, combined, 0, this.length);
System.arraycopy(bytes, 0, combined, this.length, bytes.length);
return new AsciiBytes(combined);
}
@Override
public String toString() {
if (this.string == null) {
this.string = new String(this.bytes, this.offset, this.length, UTF_8);
}
return this.string;
}
@Override
public int hashCode() {
int hash = INITIAL_HASH;
for (int i = 0; i < this.length; i++) {
hash = MULTIPLIER * hash + this.bytes[this.offset + i];
}
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj.getClass().equals(AsciiBytes.class)) {
AsciiBytes other = (AsciiBytes) obj;
if (this.length == other.length) {
for (int i = 0; i < this.length; i++) {
if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) {
return false;
}
}
return true;
}
}
return false;
}
}

@ -28,9 +28,11 @@ import org.springframework.boot.loader.archive.Archive;
*/ */
public class JarLauncher extends ExecutableArchiveLauncher { public class JarLauncher extends ExecutableArchiveLauncher {
private static final AsciiBytes LIB = new AsciiBytes("lib/");
@Override @Override
protected boolean isNestedArchive(Archive.Entry entry) { protected boolean isNestedArchive(Archive.Entry entry) {
return !entry.isDirectory() && entry.getName().startsWith("lib/"); return !entry.isDirectory() && entry.getName().startsWith(LIB);
} }
@Override @Override

@ -23,7 +23,7 @@ import java.security.AccessController;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.Enumeration; import java.util.Enumeration;
import org.springframework.boot.loader.jar.RandomAccessJarFile; import org.springframework.boot.loader.jar.JarFile;
/** /**
* {@link ClassLoader} used by the {@link Launcher}. * {@link ClassLoader} used by the {@link Launcher}.
@ -161,14 +161,16 @@ public class LaunchedURLClassLoader extends URLClassLoader {
String path = name.replace('.', '/').concat(".class"); String path = name.replace('.', '/').concat(".class");
for (URL url : getURLs()) { for (URL url : getURLs()) {
try { try {
if (url.getContent() instanceof RandomAccessJarFile) { if (url.getContent() instanceof JarFile) {
RandomAccessJarFile jarFile = (RandomAccessJarFile) url JarFile jarFile = (JarFile) url.getContent();
.getContent(); // Check the jar entry data before needlessly creating the
if (jarFile.getManifest() != null // manifest
&& jarFile.getJarEntry(path) != null) { if (jarFile.getJarEntryData(path) != null
&& jarFile.getManifest() != null) {
definePackage(packageName, jarFile.getManifest(), url); definePackage(packageName, jarFile.getManifest(), url);
return null; return null;
} }
} }
} }
catch (IOException e) { catch (IOException e) {

@ -421,10 +421,15 @@ public class PropertiesLauncher extends Launcher {
* classpath entries). * classpath entries).
*/ */
private static final class ArchiveEntryFilter implements EntryFilter { private static final class ArchiveEntryFilter implements EntryFilter {
private static final AsciiBytes DOT_JAR = new AsciiBytes(".jar");
private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip");
@Override @Override
public boolean matches(Entry entry) { public boolean matches(Entry entry) {
return entry.isDirectory() || entry.getName().endsWith(".jar") return entry.isDirectory() || entry.getName().endsWith(DOT_JAR)
|| entry.getName().endsWith(".zip"); || entry.getName().endsWith(DOT_ZIP);
} }
} }
@ -433,11 +438,13 @@ public class PropertiesLauncher extends Launcher {
* (e.g. "lib/"). * (e.g. "lib/").
*/ */
private static final class PrefixMatchingArchiveFilter implements EntryFilter { private static final class PrefixMatchingArchiveFilter implements EntryFilter {
private final String prefix;
private final AsciiBytes prefix;
private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
private PrefixMatchingArchiveFilter(String prefix) { private PrefixMatchingArchiveFilter(String prefix) {
this.prefix = prefix; this.prefix = new AsciiBytes(prefix);
} }
@Override @Override

@ -30,14 +30,25 @@ import org.springframework.boot.loader.archive.Archive;
*/ */
public class WarLauncher extends ExecutableArchiveLauncher { public class WarLauncher extends ExecutableArchiveLauncher {
private static final AsciiBytes WEB_INF = new AsciiBytes("WEB-INF/");
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
private static final AsciiBytes WEB_INF_CLASSES = WEB_INF.append("classes/");
private static final AsciiBytes WEB_INF_LIB = WEB_INF.append("lib/");
private static final AsciiBytes WEB_INF_LIB_PROVIDED = WEB_INF
.append("lib-provided/");
@Override @Override
public boolean isNestedArchive(Archive.Entry entry) { public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
return entry.getName().equals("WEB-INF/classes/"); return entry.getName().equals(WEB_INF_CLASSES);
} }
else { else {
return entry.getName().startsWith("WEB-INF/lib/") return entry.getName().startsWith(WEB_INF_LIB)
|| entry.getName().startsWith("WEB-INF/lib-provided/"); || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
} }
} }
@ -55,8 +66,8 @@ public class WarLauncher extends ExecutableArchiveLauncher {
protected Archive getFilteredArchive() throws IOException { protected Archive getFilteredArchive() throws IOException {
return getArchive().getFilteredArchive(new Archive.EntryRenameFilter() { return getArchive().getFilteredArchive(new Archive.EntryRenameFilter() {
@Override @Override
public String apply(String entryName, Archive.Entry entry) { public AsciiBytes apply(AsciiBytes entryName, Archive.Entry entry) {
if (entryName.startsWith("META-INF/") || entryName.startsWith("WEB-INF/")) { if (entryName.startsWith(META_INF) || entryName.startsWith(WEB_INF)) {
return null; return null;
} }
return entryName; return entryName;

@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.Launcher; import org.springframework.boot.loader.Launcher;
/** /**
@ -115,7 +116,7 @@ public abstract class Archive {
* Returns the name of the entry * Returns the name of the entry
* @return the name of the entry * @return the name of the entry
*/ */
String getName(); AsciiBytes getName();
} }
@ -146,7 +147,7 @@ public abstract class Archive {
* @return the new name of the entry or {@code null} if the entry should not be * @return the new name of the entry or {@code null} if the entry should not be
* included. * included.
*/ */
String apply(String entryName, Entry entry); AsciiBytes apply(AsciiBytes entryName, Entry entry);
} }

@ -35,6 +35,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.AsciiBytes;
/** /**
* {@link Archive} implementation backed by an exploded archive directory. * {@link Archive} implementation backed by an exploded archive directory.
* *
@ -45,11 +47,12 @@ public class ExplodedArchive extends Archive {
private static final Set<String> SKIPPED_NAMES = new HashSet<String>(Arrays.asList( private static final Set<String> SKIPPED_NAMES = new HashSet<String>(Arrays.asList(
".", "..")); ".", ".."));
private static final Object MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; private static final AsciiBytes MANIFEST_ENTRY_NAME = new AsciiBytes(
"META-INF/MANIFEST.MF");
private File root; private File root;
private Map<String, Entry> entries = new LinkedHashMap<String, Entry>(); private Map<AsciiBytes, Entry> entries = new LinkedHashMap<AsciiBytes, Entry>();
private Manifest manifest; private Manifest manifest;
@ -62,7 +65,7 @@ public class ExplodedArchive extends Archive {
this.entries = Collections.unmodifiableMap(this.entries); this.entries = Collections.unmodifiableMap(this.entries);
} }
private ExplodedArchive(File root, Map<String, Entry> entries) { private ExplodedArchive(File root, Map<AsciiBytes, Entry> entries) {
this.root = root; this.root = root;
this.entries = Collections.unmodifiableMap(entries); this.entries = Collections.unmodifiableMap(entries);
} }
@ -74,7 +77,8 @@ public class ExplodedArchive extends Archive {
if (file.isDirectory()) { if (file.isDirectory()) {
name += "/"; name += "/";
} }
this.entries.put(name, new FileEntry(name, file)); FileEntry entry = new FileEntry(new AsciiBytes(name), file);
this.entries.put(entry.getName(), entry);
} }
if (file.isDirectory()) { if (file.isDirectory()) {
for (File child : file.listFiles()) { for (File child : file.listFiles()) {
@ -129,9 +133,9 @@ public class ExplodedArchive extends Archive {
@Override @Override
public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException { public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException {
Map<String, Entry> filteredEntries = new LinkedHashMap<String, Archive.Entry>(); Map<AsciiBytes, Entry> filteredEntries = new LinkedHashMap<AsciiBytes, Archive.Entry>();
for (Map.Entry<String, Entry> entry : this.entries.entrySet()) { for (Map.Entry<AsciiBytes, Entry> entry : this.entries.entrySet()) {
String filteredName = filter.apply(entry.getKey(), entry.getValue()); AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue());
if (filteredName != null) { if (filteredName != null) {
filteredEntries.put(filteredName, new FileEntry(filteredName, filteredEntries.put(filteredName, new FileEntry(filteredName,
((FileEntry) entry.getValue()).getFile())); ((FileEntry) entry.getValue()).getFile()));
@ -142,10 +146,11 @@ public class ExplodedArchive extends Archive {
private class FileEntry implements Entry { private class FileEntry implements Entry {
private final String name; private final AsciiBytes name;
private final File file; private final File file;
public FileEntry(String name, File file) { public FileEntry(AsciiBytes name, File file) {
this.name = name; this.name = name;
this.file = file; this.file = file;
} }
@ -160,7 +165,7 @@ public class ExplodedArchive extends Archive {
} }
@Override @Override
public String getName() { public AsciiBytes getName() {
return this.name; return this.name;
} }
} }
@ -177,7 +182,7 @@ public class ExplodedArchive extends Archive {
protected URLConnection openConnection(URL url) throws IOException { protected URLConnection openConnection(URL url) throws IOException {
String name = url.getPath().substring( String name = url.getPath().substring(
ExplodedArchive.this.root.getAbsolutePath().length() + 1); ExplodedArchive.this.root.getAbsolutePath().length() + 1);
if (ExplodedArchive.this.entries.containsKey(name)) { if (ExplodedArchive.this.entries.containsKey(new AsciiBytes(name))) {
return new URL(url.toString()).openConnection(); return new URL(url.toString()).openConnection();
} }
return new FileNotFoundURLConnection(url, name); return new FileNotFoundURLConnection(url, name);

@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.AsciiBytes;
/** /**
* @author Dave Syer * @author Dave Syer
*/ */
@ -79,7 +81,7 @@ public class FilteredArchive extends Archive {
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
return this.parent.getFilteredArchive(new EntryRenameFilter() { return this.parent.getFilteredArchive(new EntryRenameFilter() {
@Override @Override
public String apply(String entryName, Entry entry) { public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
return FilteredArchive.this.filter.matches(entry) ? filter.apply( return FilteredArchive.this.filter.matches(entry) ? filter.apply(
entryName, entry) : null; entryName, entry) : null;
} }

@ -23,35 +23,35 @@ import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.jar.JarEntryData;
import org.springframework.boot.loader.jar.JarEntryFilter; import org.springframework.boot.loader.jar.JarEntryFilter;
import org.springframework.boot.loader.jar.RandomAccessJarFile; import org.springframework.boot.loader.jar.JarFile;
/** /**
* {@link Archive} implementation backed by a {@link RandomAccessJarFile}. * {@link Archive} implementation backed by a {@link JarFile}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class JarFileArchive extends Archive { public class JarFileArchive extends Archive {
private final RandomAccessJarFile jarFile; private final JarFile jarFile;
private final List<Entry> entries; private final List<Entry> entries;
public JarFileArchive(File file) throws IOException { public JarFileArchive(File file) throws IOException {
this(new RandomAccessJarFile(file)); this(new JarFile(file));
} }
public JarFileArchive(RandomAccessJarFile jarFile) { public JarFileArchive(JarFile jarFile) {
this.jarFile = jarFile; this.jarFile = jarFile;
ArrayList<Entry> jarFileEntries = new ArrayList<Entry>(); ArrayList<Entry> jarFileEntries = new ArrayList<Entry>();
Enumeration<JarEntry> entries = jarFile.entries(); for (JarEntryData data : jarFile) {
while (entries.hasMoreElements()) { jarFileEntries.add(new JarFileEntry(data));
jarFileEntries.add(new JarFileEntry(entries.nextElement()));
} }
this.entries = Collections.unmodifiableList(jarFileEntries); this.entries = Collections.unmodifiableList(jarFileEntries);
} }
@ -83,20 +83,19 @@ public class JarFileArchive extends Archive {
} }
protected Archive getNestedArchive(Entry entry) throws IOException { protected Archive getNestedArchive(Entry entry) throws IOException {
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
RandomAccessJarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); JarFile jarFile = this.jarFile.getNestedJarFile(data);
return new JarFileArchive(jarFile); return new JarFileArchive(jarFile);
} }
@Override @Override
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
RandomAccessJarFile filteredJar = this.jarFile JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
.getFilteredJarFile(new JarEntryFilter() { @Override
@Override public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
public String apply(String name, JarEntry entry) { return filter.apply(name, new JarFileEntry(entryData));
return filter.apply(name, new JarFileEntry(entry)); }
} });
});
return new JarFileArchive(filteredJar); return new JarFileArchive(filteredJar);
} }
@ -105,24 +104,24 @@ public class JarFileArchive extends Archive {
*/ */
private static class JarFileEntry implements Entry { private static class JarFileEntry implements Entry {
private final JarEntry jarEntry; private final JarEntryData entryData;
public JarFileEntry(JarEntry jarEntry) { public JarFileEntry(JarEntryData entryData) {
this.jarEntry = jarEntry; this.entryData = entryData;
} }
public JarEntry getJarEntry() { public JarEntryData getJarEntryData() {
return this.jarEntry; return this.entryData;
} }
@Override @Override
public boolean isDirectory() { public boolean isDirectory() {
return this.jarEntry.isDirectory(); return this.entryData.isDirectory();
} }
@Override @Override
public String getName() { public AsciiBytes getName() {
return this.jarEntry.getName(); return this.entryData.getName();
} }
} }

@ -43,7 +43,7 @@ public class ByteArrayRandomAccessData implements RandomAccessData {
} }
@Override @Override
public InputStream getInputStream() { public InputStream getInputStream(ResourceAccess access) {
return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length); return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length);
} }

@ -16,6 +16,7 @@
package org.springframework.boot.loader.data; package org.springframework.boot.loader.data;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
@ -29,9 +30,11 @@ public interface RandomAccessData {
/** /**
* Returns an {@link InputStream} that can be used to read the underling data. The * Returns an {@link InputStream} that can be used to read the underling data. The
* caller is responsible close the underlying stream. * caller is responsible close the underlying stream.
* @param access hint indicating how the underlying data should be accessed
* @return a new input stream that can be used to read the underlying data. * @return a new input stream that can be used to read the underlying data.
* @throws IOException
*/ */
InputStream getInputStream(); InputStream getInputStream(ResourceAccess access) throws IOException;
/** /**
* Returns a new {@link RandomAccessData} for a specific subsection of this data. * Returns a new {@link RandomAccessData} for a specific subsection of this data.
@ -47,4 +50,20 @@ public interface RandomAccessData {
*/ */
long getSize(); long getSize();
/**
* Lock modes for accessing the underlying resource.
*/
public static enum ResourceAccess {
/**
* Obtain access to the underlying resource once and keep it until the stream is
* closed.
*/
ONCE,
/**
* Obtain access to the underlying resource on each read, releasing it when done.
*/
PER_READ
}
} }

@ -33,7 +33,7 @@ public class RandomAccessDataFile implements RandomAccessData {
private static final int DEFAULT_CONCURRENT_READS = 4; private static final int DEFAULT_CONCURRENT_READS = 4;
private File file; private final File file;
private final FilePool filePool; private final FilePool filePool;
@ -78,7 +78,8 @@ public class RandomAccessDataFile implements RandomAccessData {
* @param offset the offset of the section * @param offset the offset of the section
* @param length the length of the section * @param length the length of the section
*/ */
private RandomAccessDataFile(FilePool pool, long offset, long length) { private RandomAccessDataFile(File file, FilePool pool, long offset, long length) {
this.file = file;
this.filePool = pool; this.filePool = pool;
this.offset = offset; this.offset = offset;
this.length = length; this.length = length;
@ -93,8 +94,8 @@ public class RandomAccessDataFile implements RandomAccessData {
} }
@Override @Override
public InputStream getInputStream() { public InputStream getInputStream(ResourceAccess access) throws IOException {
return new DataInputStream(); return new DataInputStream(access);
} }
@Override @Override
@ -102,7 +103,8 @@ public class RandomAccessDataFile implements RandomAccessData {
if (offset < 0 || length < 0 || offset + length > this.length) { if (offset < 0 || length < 0 || offset + length > this.length) {
throw new IndexOutOfBoundsException(); throw new IndexOutOfBoundsException();
} }
return new RandomAccessDataFile(this.filePool, this.offset + offset, length); return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset,
length);
} }
@Override @Override
@ -120,7 +122,16 @@ public class RandomAccessDataFile implements RandomAccessData {
*/ */
private class DataInputStream extends InputStream { private class DataInputStream extends InputStream {
private long position; private RandomAccessFile file;
private int position;
public DataInputStream(ResourceAccess access) throws IOException {
if (access == ResourceAccess.ONCE) {
this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r");
this.file.seek(RandomAccessDataFile.this.offset);
}
}
@Override @Override
public int read() throws IOException { public int read() throws IOException {
@ -153,23 +164,29 @@ public class RandomAccessDataFile implements RandomAccessData {
if (len == 0) { if (len == 0) {
return 0; return 0;
} }
if (cap(len) <= 0) { int cappedLen = cap(len);
if (cappedLen <= 0) {
return -1; return -1;
} }
RandomAccessFile file = RandomAccessDataFile.this.filePool.acquire(); RandomAccessFile file = this.file;
try { if (file == null) {
file = RandomAccessDataFile.this.filePool.acquire();
file.seek(RandomAccessDataFile.this.offset + this.position); file.seek(RandomAccessDataFile.this.offset + this.position);
}
try {
if (b == null) { if (b == null) {
int rtn = file.read(); int rtn = file.read();
moveOn(rtn == -1 ? 0 : 1); moveOn(rtn == -1 ? 0 : 1);
return rtn; return rtn;
} }
else { else {
return (int) moveOn(file.read(b, off, (int) cap(len))); return (int) moveOn(file.read(b, off, cappedLen));
} }
} }
finally { finally {
RandomAccessDataFile.this.filePool.release(file); if (this.file == null) {
RandomAccessDataFile.this.filePool.release(file);
}
} }
} }
@ -178,14 +195,21 @@ public class RandomAccessDataFile implements RandomAccessData {
return (n <= 0 ? 0 : moveOn(cap(n))); return (n <= 0 ? 0 : moveOn(cap(n)));
} }
@Override
public void close() throws IOException {
if (this.file != null) {
this.file.close();
}
}
/** /**
* Cap the specified value such that it cannot exceed the number of bytes * Cap the specified value such that it cannot exceed the number of bytes
* remaining. * remaining.
* @param n the value to cap * @param n the value to cap
* @return the capped value * @return the capped value
*/ */
private long cap(long n) { private int cap(long n) {
return Math.min(RandomAccessDataFile.this.length - this.position, n); return (int) Math.min(RandomAccessDataFile.this.length - this.position, n);
} }
/** /**
@ -193,7 +217,7 @@ public class RandomAccessDataFile implements RandomAccessData {
* @param amount the amount to move * @param amount the amount to move
* @return the amount moved * @return the amount moved
*/ */
private long moveOn(long amount) { private long moveOn(int amount) {
this.position += amount; this.position += amount;
return amount; return amount;
} }
@ -237,16 +261,16 @@ public class RandomAccessDataFile implements RandomAccessData {
public void close() throws IOException { public void close() throws IOException {
try { try {
this.available.acquire(size); this.available.acquire(this.size);
try { try {
RandomAccessFile file = files.poll(); RandomAccessFile file = this.files.poll();
while (file != null) { while (file != null) {
file.close(); file.close();
file = files.poll(); file = this.files.poll();
} }
} }
finally { finally {
this.available.release(size); this.available.release(this.size);
} }
} }
catch (InterruptedException ex) { catch (InterruptedException ex) {

@ -0,0 +1,79 @@
/*
* Copyright 2012-2013 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 org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
/**
* Utilities for dealing with bytes from ZIP files.
*
* @author Phillip Webb
*/
class Bytes {
private static final byte[] EMPTY_BYTES = new byte[] {};
public static byte[] get(RandomAccessData data) throws IOException {
InputStream inputStream = data.getInputStream(ResourceAccess.ONCE);
try {
return get(inputStream, data.getSize());
}
finally {
inputStream.close();
}
}
public static byte[] get(InputStream inputStream, long length) throws IOException {
if (length == 0) {
return EMPTY_BYTES;
}
byte[] bytes = new byte[(int) length];
if (!fill(inputStream, bytes)) {
throw new IOException("Unable to read bytes");
}
return bytes;
}
public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException {
return fill(inputStream, bytes, 0, bytes.length);
}
private static boolean fill(InputStream inputStream, byte[] bytes, int offset,
int length) throws IOException {
while (length > 0) {
int read = inputStream.read(bytes, offset, length);
if (read == -1) {
return false;
}
offset += read;
length = -read;
}
return true;
}
public static long littleEndianValue(byte[] bytes, int offset, int length) {
long value = 0;
for (int i = length - 1; i >= 0; i--) {
value = ((value << 8) | (bytes[offset + i] & 0xFF));
}
return value;
}
}

@ -0,0 +1,108 @@
/*
* Copyright 2012-2013 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 org.springframework.boot.loader.data.RandomAccessData;
/**
* A ZIP File "End of central directory record" (EOCD).
*
* @author Phillip Webb
* @see <a href="http://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
*/
class CentralDirectoryEndRecord {
private static final int MINIMUM_SIZE = 22;
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
private static final int SIGNATURE = 0x06054b50;
private static final int COMMENT_LENGTH_OFFSET = 20;
private static final int READ_BLOCK_SIZE = 256;
private byte[] block;
private int offset;
private int size;
/**
* Create a new {@link CentralDirectoryEndRecord} instance from the specified
* {@link RandomAccessData}, searching backwards from the end until a valid block is
* located.
* @param data the source data
* @throws IOException
*/
public CentralDirectoryEndRecord(RandomAccessData data) throws IOException {
this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);
this.size = MINIMUM_SIZE;
this.offset = this.block.length - this.size;
while (!isValid()) {
this.size++;
if (this.size > this.block.length) {
if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {
throw new IOException("Unable to find ZIP central directory "
+ "records after reading " + this.size + " bytes");
}
this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);
}
this.offset = this.block.length - this.size;
}
}
private byte[] createBlockFromEndOfData(RandomAccessData data, int size)
throws IOException {
int length = (int) Math.min(data.getSize(), size);
return Bytes.get(data.getSubsection(data.getSize() - length, length));
}
private boolean isValid() {
if (this.block.length < MINIMUM_SIZE
|| Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {
return false;
}
// Total size must be the structure size + comment
long commentLength = Bytes.littleEndianValue(this.block, this.offset
+ COMMENT_LENGTH_OFFSET, 2);
return this.size == MINIMUM_SIZE + commentLength;
}
/**
* Return the bytes of the "Central directory" based on the offset indicated in this
* record.
* @param data the source data
* @return the central directory data
*/
public RandomAccessData getCentralDirectory(RandomAccessData data) {
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
return data.getSubsection(offset, length);
}
/**
* Return the number of ZIP entries in the file.
*/
public int getNumberOfRecords() {
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2);
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2013 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.security.CodeSigner;
import java.security.cert.Certificate;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
*
* @author Phillip Webb
*/
public class JarEntry extends java.util.jar.JarEntry {
private final JarEntryData source;
private Certificate[] certificates;
private CodeSigner[] codeSigners;
public JarEntry(JarEntryData source) {
super(source.getName().toString());
this.source = source;
}
/**
* Return the source {@link JarEntryData} that was used to create this entry.
*/
public JarEntryData getSource() {
return this.source;
}
@Override
public Attributes getAttributes() throws IOException {
Manifest manifest = this.source.getSource().getManifest();
return (manifest == null ? null : manifest.getAttributes(getName()));
}
@Override
public Certificate[] getCertificates() {
if (this.source.getSource().isSigned() && this.certificates == null) {
this.source.getSource().setupEntryCertificates();
}
return this.certificates;
}
@Override
public CodeSigner[] getCodeSigners() {
if (this.source.getSource().isSigned() && this.codeSigners == null) {
this.source.getSource().setupEntryCertificates();
}
return this.codeSigners;
}
void setupCertificates(java.util.jar.JarEntry entry) {
this.certificates = entry.getCertificates();
this.codeSigners = entry.getCodeSigners();
}
}

@ -0,0 +1,168 @@
/*
* Copyright 2012-2013 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.lang.ref.SoftReference;
import java.util.zip.ZipEntry;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
/**
* Holds the underlying data of a {@link JarEntry}, allowing creation to be deferred until
* the entry is actually needed.
*
* @author Phillip Webb
*/
public final class JarEntryData {
private static final long LOCAL_FILE_HEADER_SIZE = 30;
private static final AsciiBytes SLASH = new AsciiBytes("/");
private final JarFile source;
private byte[] header;
private AsciiBytes name;
private final byte[] extra;
private final AsciiBytes comment;
private long dataOffset;
private RandomAccessData data;
private SoftReference<JarEntry> entry;
public JarEntryData(JarFile source, 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);
long commentLength = Bytes.littleEndianValue(header, 32, 2);
this.name = new AsciiBytes(Bytes.get(inputStream, nameLength));
this.extra = Bytes.get(inputStream, extraLength);
this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength));
this.dataOffset = Bytes.littleEndianValue(header, 42, 4);
this.dataOffset += LOCAL_FILE_HEADER_SIZE;
this.dataOffset += this.name.length();
this.dataOffset += this.extra.length;
}
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;
}
RandomAccessData getData() {
if (this.data == null) {
this.data = this.source.getData().getSubsection(this.dataOffset,
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;
}
public boolean isDirectory() {
return this.name.endsWith(SLASH);
}
public int getMethod() {
return (int) Bytes.littleEndianValue(this.header, 10, 2);
}
public long getTime() {
return Bytes.littleEndianValue(this.header, 12, 4);
}
public long getCrc() {
return Bytes.littleEndianValue(this.header, 16, 4);
}
public int getCompressedSize() {
return (int) Bytes.littleEndianValue(this.header, 20, 4);
}
public int getSize() {
return (int) Bytes.littleEndianValue(this.header, 24, 4);
}
public byte[] getExtra() {
return this.extra;
}
public AsciiBytes getComment() {
return this.comment;
}
/**
* Create a new {@link JarEntryData} instance from the specified input stream.
* @param source the source {@link JarFile}
* @param inputStream the input stream to load data from
* @return a {@link JarEntryData} or {@code null}
* @throws IOException
*/
static JarEntryData fromInputStream(JarFile source, InputStream inputStream)
throws IOException {
byte[] header = new byte[46];
if (!Bytes.fill(inputStream, header)) {
return null;
}
return new JarEntryData(source, header, inputStream);
}
}

@ -16,7 +16,7 @@
package org.springframework.boot.loader.jar; package org.springframework.boot.loader.jar;
import java.util.jar.JarEntry; import org.springframework.boot.loader.AsciiBytes;
/** /**
* Interface that can be used to filter and optionally rename jar entries. * Interface that can be used to filter and optionally rename jar entries.
@ -27,12 +27,12 @@ public interface JarEntryFilter {
/** /**
* Apply the jar entry filter. * Apply the jar entry filter.
* @param entryName the current entry name. This may be different that the original * @param name the current entry name. This may be different that the original entry
* entry name if a previous filter has been applied * name if a previous filter has been applied
* @param entry the entry to filter * @param entryData the entry data to filter
* @return the new name of the entry or {@code null} if the entry should not be * @return the new name of the entry or {@code null} if the entry should not be
* included. * included.
*/ */
String apply(String entryName, JarEntry entry); AsciiBytes apply(AsciiBytes name, JarEntryData entryData);
} }

@ -0,0 +1,387 @@
/*
* Copyright 2012-2013 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.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
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;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.data.RandomAccessDataFile;
/**
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
* existing files.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
* entry.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) 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> {
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 final RandomAccessDataFile rootFile;
private RandomAccessData data;
private final String name;
private final long size;
private boolean signed;
private List<JarEntryData> entries;
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
private JarEntryData manifestEntry;
private SoftReference<Manifest> manifest;
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException
*/
public JarFile(File file, JarEntryFilter... filters) throws IOException {
this(new RandomAccessDataFile(file), filters);
}
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException
*/
JarFile(RandomAccessDataFile file, JarEntryFilter... filters) throws IOException {
this(file, file.getFile().getPath(), file, filters);
}
/**
* Private constructor used to create a new {@link JarFile} either directly or from a
* nested entry.
* @param rootFile the root jar file
* @param name the name of this file
* @param data the underlying data
* @param filters an optional set of jar entry filters
* @throws IOException
*/
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data,
JarEntryFilter... filters) throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.name = name;
this.data = data;
this.size = data.getSize();
loadJarEntries(filters);
}
private void loadJarEntries(JarEntryFilter[] filters) throws IOException {
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(this.data);
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
int numberOfRecords = endRecord.getNumberOfRecords();
this.entries = new ArrayList<JarEntryData>(numberOfRecords);
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
try {
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
while (entry != null) {
addJarEntry(entry, filters);
entry = JarEntryData.fromInputStream(this, inputStream);
}
}
finally {
inputStream.close();
}
}
private void addJarEntry(JarEntryData entry, JarEntryFilter[] filters) {
AsciiBytes name = entry.getName();
for (JarEntryFilter filter : filters) {
name = (filter == null || name == null ? name : filter.apply(name, entry));
}
if (name != null) {
entry.setName(name);
this.entries.add(entry);
if (name.startsWith(META_INF)) {
processMetaInfEntry(name, entry);
}
}
}
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
if (name.equals(MANIFEST_MF)) {
this.manifestEntry = entry;
}
if (name.endsWith(SIGNATURE_FILE_EXTENSION)) {
this.signed = true;
}
}
protected final RandomAccessDataFile getRootJarFile() {
return this.rootFile;
}
RandomAccessData getData() {
return this.data;
}
@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();
try {
manifest = new Manifest(inputStream);
}
finally {
inputStream.close();
}
this.manifest = new SoftReference<Manifest>(manifest);
}
return manifest;
}
@Override
public Enumeration<java.util.jar.JarEntry> entries() {
final Iterator<JarEntryData> iterator = iterator();
return new Enumeration<java.util.jar.JarEntry>() {
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public java.util.jar.JarEntry nextElement() {
return iterator.next().asJarEntry();
}
};
}
@Override
public Iterator<JarEntryData> iterator() {
return this.entries.iterator();
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
JarEntryData jarEntryData = getJarEntryData(name);
return (jarEntryData == null ? null : jarEntryData.asJarEntry());
}
public JarEntryData getJarEntryData(String name) {
if (name == null) {
return null;
}
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(new AsciiBytes(name));
if (entryData == null && !name.endsWith("/")) {
entryData = entriesByName.get(new AsciiBytes(name + "/"));
}
return entryData;
}
boolean isSigned() {
return this.signed;
}
void setupEntryCertificates() {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try {
JarInputStream inputStream = new JarInputStream(getData().getInputStream(
ResourceAccess.ONCE));
try {
java.util.jar.JarEntry entry = inputStream.getNextJarEntry();
while (entry != null) {
inputStream.closeEntry();
JarEntry jarEntry = getJarEntry(entry.getName());
if (jarEntry != null) {
jarEntry.setupCertificates(entry);
}
entry = inputStream.getNextJarEntry();
}
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
return getContainedEntry(ze).getSource().getInputStream();
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param ze the zip entry
* @param filters an optional set of jar entry filters to be applied
* @return a {@link JarFile} for the entry
* @throws IOException
*/
public synchronized JarFile getNestedJarFile(final ZipEntry ze,
JarEntryFilter... filters) throws IOException {
return getNestedJarFile(getContainedEntry(ze).getSource());
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param sourceEntry the zip entry
* @param filters an optional set of jar entry filters to be applied
* @return a {@link JarFile} for the entry
* @throws IOException
*/
public synchronized JarFile getNestedJarFile(final JarEntryData sourceEntry,
JarEntryFilter... filters) throws IOException {
if (sourceEntry.isDirectory()) {
return getNestedJarFileFromDirectoryEntry(sourceEntry, filters);
}
return getNestedJarFileFromFileEntry(sourceEntry, filters);
}
private JarFile getNestedJarFileFromDirectoryEntry(JarEntryData sourceEntry,
JarEntryFilter... filters) throws IOException {
final AsciiBytes sourceName = sourceEntry.getName();
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1];
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
filtersToUse[0] = new JarEntryFilter() {
@Override
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
return name.substring(sourceName.length());
}
return null;
}
};
return new JarFile(this.rootFile, getName() + "!/"
+ sourceEntry.getName().substring(0, sourceName.length() - 1), this.data,
filtersToUse);
}
private JarFile getNestedJarFileFromFileEntry(JarEntryData sourceEntry,
JarEntryFilter... filters) throws IOException {
if (sourceEntry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException("Unable to open nested compressed entry "
+ sourceEntry.getName());
}
return new JarFile(this.rootFile, getName() + "!/" + sourceEntry.getName(),
sourceEntry.getData(), filters);
}
/**
* 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
*/
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
throws IOException {
return new JarFile(this.rootFile, getName(), this.data, filters);
}
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
if (zipEntry instanceof JarEntry
&& ((JarEntry) zipEntry).getSource().getSource() == this) {
return (JarEntry) zipEntry;
}
throw new IllegalArgumentException("ZipEntry must be contained in this file");
}
@Override
public String getName() {
return this.name;
}
@Override
public int size() {
return (int) this.size;
}
@Override
public void close() throws IOException {
this.rootFile.close();
}
@Override
public String toString() {
return getName();
}
/**
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
* cannot be serialized and or cloned.
* @return the URL
* @throws MalformedURLException
*/
public URL getUrl() throws MalformedURLException {
JarURLStreamHandler handler = new JarURLStreamHandler(this);
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
}
}

@ -0,0 +1,133 @@
/*
* Copyright 2012-2013 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.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
/**
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
*
* @author Phillip Webb
*/
class JarURLConnection extends java.net.JarURLConnection {
private static final String JAR_URL_POSTFIX = "!/";
private static final String JAR_URL_PREFIX = "jar:file:";
private JarFile jarFile;
private JarEntryData jarEntryData;
private String jarEntryName;
private String contentType;
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
super(new URL(buildRootUrl(jarFile)));
this.jarFile = jarFile;
String spec = url.getFile();
int separator = spec.lastIndexOf(JAR_URL_POSTFIX);
if (separator == -1) {
throw new MalformedURLException("no !/ found in url spec:" + spec);
}
if (separator + 2 != spec.length()) {
this.jarEntryName = spec.substring(separator + 2);
}
}
@Override
public void connect() throws IOException {
if (this.jarEntryName != null) {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName);
if (this.jarEntryData == null) {
throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName());
}
}
this.connected = true;
}
@Override
public JarFile getJarFile() throws IOException {
connect();
return this.jarFile;
}
@Override
public JarEntry getJarEntry() throws IOException {
connect();
return (this.jarEntryData == null ? null : this.jarEntryData.asJarEntry());
}
@Override
public InputStream getInputStream() throws IOException {
connect();
if (this.jarEntryName == null) {
throw new IOException("no entry name specified");
}
return this.jarEntryData.getInputStream();
}
@Override
public int getContentLength() {
try {
connect();
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData
.getSize();
}
catch (IOException ex) {
return -1;
}
}
@Override
public Object getContent() throws IOException {
connect();
return (this.jarEntryData == null ? this.jarFile : super.getContent());
}
@Override
public String getContentType() {
if (this.contentType == null) {
// Guess the content type, don't bother with steams as mark is not
// supported
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
: this.contentType);
this.contentType = (this.contentType == null ? "content/unknown"
: this.contentType);
}
return this.contentType;
}
private static String buildRootUrl(JarFile jarFile) {
String path = jarFile.getRootJarFile().getFile().getPath();
StringBuilder builder = new StringBuilder(JAR_URL_PREFIX.length() + path.length()
+ JAR_URL_POSTFIX.length());
builder.append(JAR_URL_PREFIX);
builder.append(path);
builder.append(JAR_URL_POSTFIX);
return builder.toString();
}
}

@ -16,34 +16,26 @@
package org.springframework.boot.loader.jar; package org.springframework.boot.loader.jar;
import java.util.jar.JarEntry; import java.io.IOException;
import java.net.URL;
import org.springframework.boot.loader.data.RandomAccessData; import java.net.URLConnection;
import java.net.URLStreamHandler;
/** /**
* A {@link JarEntry} returned from a {@link RandomAccessDataJarInputStream}. * {@link URLStreamHandler} used to support {@link JarFile#getUrl()}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class RandomAccessDataJarEntry extends JarEntry { class JarURLStreamHandler extends URLStreamHandler {
private RandomAccessData data; private JarFile jarFile;
/** public JarURLStreamHandler(JarFile jarFile) {
* Create new {@link RandomAccessDataJarEntry} instance. this.jarFile = jarFile;
* @param entry the underlying {@link JarEntry}
* @param data the entry data
*/
public RandomAccessDataJarEntry(JarEntry entry, RandomAccessData data) {
super(entry);
this.data = data;
} }
/** @Override
* Returns the {@link RandomAccessData} for this entry. protected URLConnection openConnection(URL url) throws IOException {
* @return the entry data return new JarURLConnection(url, this.jarFile);
*/
public RandomAccessData getData() {
return this.data;
} }
} }

@ -1,124 +0,0 @@
/*
* Copyright 2012-2013 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.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import org.springframework.boot.loader.data.RandomAccessData;
/**
* A {@link JarInputStream} backed by {@link RandomAccessData}. Parsed entries provide
* access to the underlying data {@link RandomAccessData#getSubsection(long, long)
* subsection}.
*
* @author Phillip Webb
*/
public class RandomAccessDataJarInputStream extends JarInputStream {
private RandomAccessData data;
private TrackingInputStream trackingInputStream;
/**
* Create a new {@link RandomAccessData} instance.
* @param data the source of the zip stream
* @throws IOException
*/
public RandomAccessDataJarInputStream(RandomAccessData data) throws IOException {
this(data, new TrackingInputStream(data.getInputStream()));
}
/**
* Private constructor used so that we can call the super constructor with a
* {@link TrackingInputStream}.
* @param data the source of the zip stream
* @param trackingInputStream a tracking input stream
* @throws IOException
*/
private RandomAccessDataJarInputStream(RandomAccessData data,
TrackingInputStream trackingInputStream) throws IOException {
super(trackingInputStream);
this.data = data;
this.trackingInputStream = trackingInputStream;
}
@Override
public RandomAccessDataJarEntry getNextEntry() throws IOException {
JarEntry entry = (JarEntry) super.getNextEntry();
if (entry == null) {
return null;
}
int start = getPosition();
closeEntry();
int end = getPosition();
RandomAccessData entryData = this.data.getSubsection(start, end - start);
return new RandomAccessDataJarEntry(entry, entryData);
}
private int getPosition() throws IOException {
int pushback = ((PushbackInputStream) this.in).available();
return this.trackingInputStream.getPosition() - pushback;
}
/**
* Internal stream that tracks reads to provide a position.
*/
private static class TrackingInputStream extends FilterInputStream {
private int position = 0;
protected TrackingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
return moveOn(super.read(), true);
}
@Override
public int read(byte[] b) throws IOException {
return moveOn(super.read(b), false);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return moveOn(super.read(b, off, len), false);
}
private int moveOn(int amount, boolean singleByteRead) {
this.position += (amount == -1 ? 0 : (singleByteRead ? 1 : amount));
return amount;
}
@Override
public int available() throws IOException {
// Always return 0 so that we can accurately use PushbackInputStream.available
return 0;
}
public int getPosition() {
return this.position;
}
}
}

@ -1,510 +0,0 @@
/*
* Copyright 2012-2013 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.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.boot.loader.data.ByteArrayRandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessDataFile;
/**
* A Jar file that can loaded from a {@link RandomAccessDataFile}. This class extends and
* behaves in the same was a the standard JDK {@link JarFile} the following additional
* functionality.
* <ul>
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
* existing files.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
* entry.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) 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 RandomAccessJarFile extends JarFile {
private static final RandomAccessData EMPTY_DATA = new ByteArrayRandomAccessData(
new byte[0]);
private final RandomAccessDataFile rootJarFile;
private RandomAccessData data;
private final String name;
private final long size;
private Map<String, JarEntry> entries = new LinkedHashMap<String, JarEntry>();
private Manifest manifest;
/**
* Create a new {@link RandomAccessJarFile} backed by the specified file.
* @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException
*/
public RandomAccessJarFile(File file, JarEntryFilter... filters) throws IOException {
this(new RandomAccessDataFile(file), filters);
}
/**
* Create a new {@link RandomAccessJarFile} backed by the specified file.
* @param file the root jar file
* @param filters an optional set of jar entry filters
* @throws IOException
*/
public RandomAccessJarFile(RandomAccessDataFile file, JarEntryFilter... filters)
throws IOException {
this(file, file.getFile().getPath(), file, filters);
}
/**
* Private constructor used to create a new {@link RandomAccessJarFile} either
* directly or from a nested entry.
* @param rootJarFile the root jar file
* @param name the name of this file
* @param data the underlying data
* @param filters an optional set of jar entry filters
* @throws IOException
*/
private RandomAccessJarFile(RandomAccessDataFile rootJarFile, String name,
RandomAccessData data, JarEntryFilter... filters) throws IOException {
super(rootJarFile.getFile());
this.rootJarFile = rootJarFile;
this.name = name;
this.data = data;
this.size = data.getSize();
RandomAccessDataJarInputStream inputStream = new RandomAccessDataJarInputStream(
data);
try {
RandomAccessDataJarEntry zipEntry = inputStream.getNextEntry();
while (zipEntry != null) {
addJarEntry(zipEntry, filters);
zipEntry = inputStream.getNextEntry();
}
this.manifest = inputStream.getManifest();
if (this.manifest != null) {
addManifestEntries(filters);
}
}
finally {
inputStream.close();
}
}
private void addManifestEntries(JarEntryFilter... filters) throws IOException {
Map<String, JarEntry> originalEntries = this.entries;
this.entries = new LinkedHashMap<String, JarEntry>();
ZipInputStream zipInputStream = new ZipInputStream(this.data.getInputStream());
try {
JarEntry entry;
do {
entry = new JarEntry(zipInputStream.getNextEntry());
entry.setMethod(ZipEntry.STORED);
RandomAccessData data = EMPTY_DATA;
if (MANIFEST_NAME.equals(entry.getName())) {
ByteArrayOutputStream manifestBytes = new ByteArrayOutputStream();
this.manifest.write(manifestBytes);
manifestBytes.close();
data = new ByteArrayRandomAccessData(manifestBytes.toByteArray());
}
addJarEntry(new RandomAccessDataJarEntry(entry, data), filters);
}
while (!MANIFEST_NAME.equals(entry.getName()));
this.entries.putAll(originalEntries);
}
finally {
zipInputStream.close();
}
}
private void addJarEntry(RandomAccessDataJarEntry zipEntry, JarEntryFilter... filters) {
Entry jarEntry = new Entry(zipEntry);
String name = zipEntry.getName();
for (JarEntryFilter filter : filters) {
name = (filter == null || name == null ? name : filter.apply(name, jarEntry));
}
if (name != null) {
jarEntry.setName(name);
this.entries.put(name, jarEntry);
}
}
protected final RandomAccessDataFile getRootJarFile() {
return this.rootJarFile;
}
@Override
public Manifest getManifest() throws IOException {
return this.manifest;
}
@Override
public Enumeration<JarEntry> entries() {
return Collections.enumeration(this.entries.values());
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
JarEntry entry = this.entries.get(name);
if (entry == null && name != null && !name.endsWith("/")) {
entry = this.entries.get(name + "/");
}
return entry;
}
@Override
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
InputStream inputStream = getData(ze).getInputStream();
if (ze.getMethod() == ZipEntry.DEFLATED) {
inputStream = new ZipInflaterInputStream(inputStream, (int) ze.getSize());
}
return inputStream;
}
/**
* Return a nested {@link RandomAccessJarFile} loaded from the specified entry.
* @param ze the zip entry
* @param filters an optional set of jar entry filters to be applied
* @return a {@link RandomAccessJarFile} for the entry
* @throws IOException
*/
public synchronized RandomAccessJarFile getNestedJarFile(final ZipEntry ze,
JarEntryFilter... filters) throws IOException {
if (ze == null) {
throw new IllegalArgumentException("ZipEntry must not be null");
}
if (ze.isDirectory()) {
return getNestedJarFileFromDirectoryEntry(ze, filters);
}
return getNestedJarFileFromFileEntry(ze, filters);
}
private RandomAccessJarFile getNestedJarFileFromDirectoryEntry(final ZipEntry entry,
JarEntryFilter... filters) throws IOException {
final String name = entry.getName();
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1];
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
filtersToUse[0] = new JarEntryFilter() {
@Override
public String apply(String entryName, JarEntry ze) {
if (entryName.startsWith(name) && !entryName.equals(name)) {
return entryName.substring(entry.getName().length());
}
return null;
}
};
return new RandomAccessJarFile(this.rootJarFile, getName() + "!/"
+ name.substring(0, name.length() - 1), this.data, filtersToUse);
}
private RandomAccessJarFile getNestedJarFileFromFileEntry(ZipEntry entry,
JarEntryFilter... filters) throws IOException {
if (entry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException("Unable to open nested compressed entry "
+ entry.getName());
}
return new RandomAccessJarFile(this.rootJarFile, getName() + "!/"
+ entry.getName(), getData(entry), filters);
}
/**
* 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 RandomAccessJarFile}
* @throws IOException
*/
public synchronized RandomAccessJarFile getFilteredJarFile(JarEntryFilter... filters)
throws IOException {
return new RandomAccessJarFile(this.rootJarFile, getName(), this.data, filters);
}
/**
* Return {@link RandomAccessData} for the specified entry.
* @param ze the zip entry
* @return the entry {@link RandomAccessData}
* @throws IOException
*/
private synchronized RandomAccessData getData(ZipEntry ze) throws IOException {
if (!this.entries.containsValue(ze)) {
throw new IllegalArgumentException("ZipEntry must be contained in this file");
}
return ((Entry) ze).getData();
}
@Override
public String getName() {
return this.name;
}
@Override
public int size() {
return (int) this.size;
}
@Override
public void close() throws IOException {
this.rootJarFile.close();
}
@Override
public String toString() {
return getName();
}
/**
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
* cannot be serialized and or cloned.
* @return the URL
* @throws MalformedURLException
*/
public URL getUrl() throws MalformedURLException {
RandomAccessJarURLStreamHandler handler = new RandomAccessJarURLStreamHandler(
this);
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
}
/**
* A single {@link JarEntry} in this file.
*/
private static class Entry extends JarEntry {
private String name;
private RandomAccessData entryData;
public Entry(RandomAccessDataJarEntry entry) {
super(entry);
this.entryData = entry.getData();
}
void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return (this.name == null ? super.getName() : this.name);
}
public RandomAccessData getData() {
return this.entryData;
}
}
/**
* {@link URLStreamHandler} used to support {@link RandomAccessJarFile#getUrl()}.
*/
private static class RandomAccessJarURLStreamHandler extends URLStreamHandler {
private RandomAccessJarFile jarFile;
public RandomAccessJarURLStreamHandler(RandomAccessJarFile jarFile) {
this.jarFile = jarFile;
}
@Override
protected URLConnection openConnection(URL url) throws IOException {
return new RandomAccessJarURLConnection(url, this.jarFile);
}
}
/**
* {@link JarURLConnection} used to support {@link RandomAccessJarFile#getUrl()}.
*/
private static class RandomAccessJarURLConnection extends JarURLConnection {
private RandomAccessJarFile jarFile;
private JarEntry jarEntry;
private String jarEntryName;
private String contentType;
protected RandomAccessJarURLConnection(URL url, RandomAccessJarFile jarFile)
throws MalformedURLException {
super(new URL("jar:file:" + jarFile.getRootJarFile().getFile().getPath()
+ "!/"));
this.jarFile = jarFile;
String spec = url.getFile();
int separator = spec.lastIndexOf("!/");
if (separator == -1) {
throw new MalformedURLException("no !/ found in url spec:" + spec);
}
if (separator + 2 != spec.length()) {
this.jarEntryName = spec.substring(separator + 2);
}
}
@Override
public void connect() throws IOException {
if (this.jarEntryName != null) {
this.jarEntry = this.jarFile.getJarEntry(this.jarEntryName);
if (this.jarEntry == null) {
throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName());
}
}
this.connected = true;
}
@Override
public RandomAccessJarFile getJarFile() throws IOException {
connect();
return this.jarFile;
}
@Override
public JarEntry getJarEntry() throws IOException {
connect();
return this.jarEntry;
}
@Override
public InputStream getInputStream() throws IOException {
connect();
if (this.jarEntryName == null) {
throw new IOException("no entry name specified");
}
return this.jarFile.getInputStream(this.jarEntry);
}
@Override
public int getContentLength() {
try {
connect();
return (int) (this.jarEntry == null ? this.jarFile.size() : this.jarEntry
.getSize());
}
catch (IOException ex) {
return -1;
}
}
@Override
public Object getContent() throws IOException {
connect();
return (this.jarEntry == null ? this.jarFile : super.getContent());
}
@Override
public String getContentType() {
if (this.contentType == null) {
// Guess the content type, don't bother with steams as mark is not
// supported
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
: this.contentType);
this.contentType = (this.contentType == null ? "content/unknown"
: this.contentType);
}
return this.contentType;
}
}
/**
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte
* (which is required with JDK 6) and returns accurate available() results.
*/
private static class ZipInflaterInputStream extends InflaterInputStream {
private boolean extraBytesWritten;
private int available;
public ZipInflaterInputStream(InputStream inputStream, int size) {
super(inputStream, new Inflater(true), 512);
this.available = size;
}
@Override
public int available() throws IOException {
if (this.available < 0) {
return super.available();
}
return this.available;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
if (result != -1) {
this.available -= result;
}
return result;
}
@Override
protected void fill() throws IOException {
try {
super.fill();
}
catch (EOFException ex) {
if (this.extraBytesWritten) {
throw ex;
}
this.len = 1;
this.buf[0] = 0x0;
this.extraBytesWritten = true;
this.inf.setInput(this.buf, 0, this.len);
}
}
}
}

@ -0,0 +1,75 @@
/*
* Copyright 2012-2013 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.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
* is required with JDK 6) and returns accurate available() results.
*
* @author Phillip Webb
*/
class ZipInflaterInputStream extends InflaterInputStream {
private boolean extraBytesWritten;
private int available;
public ZipInflaterInputStream(InputStream inputStream, int size) {
super(inputStream, new Inflater(true), 512);
this.available = size;
}
@Override
public int available() throws IOException {
if (this.available < 0) {
return super.available();
}
return this.available;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
if (result != -1) {
this.available -= result;
}
return result;
}
@Override
protected void fill() throws IOException {
try {
super.fill();
}
catch (EOFException ex) {
if (this.extraBytesWritten) {
throw ex;
}
this.len = 1;
this.buf[0] = 0x0;
this.extraBytesWritten = true;
this.inf.setInput(this.buf, 0, this.len);
}
}
}

@ -0,0 +1,143 @@
/*
* Copyright 2012-2013 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;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link AsciiBytes}.
*
* @author Phillip Webb
*/
public class AsciiBytesTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void createFromBytes() throws Exception {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 });
assertThat(bytes.toString(), equalTo("AB"));
}
@Test
public void createFromBytesWithOffset() throws Exception {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(bytes.toString(), equalTo("BC"));
}
@Test
public void createFromString() throws Exception {
AsciiBytes bytes = new AsciiBytes("AB");
assertThat(bytes.toString(), equalTo("AB"));
}
@Test
public void length() throws Exception {
AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(b1.length(), equalTo(2));
assertThat(b2.length(), equalTo(2));
}
@Test
public void startWith() throws Exception {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abc.startsWith(abc), equalTo(true));
assertThat(abc.startsWith(ab), equalTo(true));
assertThat(abc.startsWith(bc), equalTo(false));
assertThat(abc.startsWith(abcd), equalTo(false));
}
@Test
public void endsWith() throws Exception {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 });
assertThat(abc.endsWith(abc), equalTo(true));
assertThat(abc.endsWith(bc), equalTo(true));
assertThat(abc.endsWith(ab), equalTo(false));
assertThat(abc.endsWith(aabc), equalTo(false));
}
@Test
public void substringFromBeingIndex() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0).toString(), equalTo("ABCD"));
assertThat(abcd.substring(1).toString(), equalTo("BCD"));
assertThat(abcd.substring(2).toString(), equalTo("CD"));
assertThat(abcd.substring(3).toString(), equalTo("D"));
assertThat(abcd.substring(4).toString(), equalTo(""));
this.thrown.expect(IndexOutOfBoundsException.class);
abcd.substring(5);
}
@Test
public void substring() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0, 4).toString(), equalTo("ABCD"));
assertThat(abcd.substring(1, 3).toString(), equalTo("BC"));
assertThat(abcd.substring(3, 4).toString(), equalTo("D"));
assertThat(abcd.substring(3, 3).toString(), equalTo(""));
this.thrown.expect(IndexOutOfBoundsException.class);
abcd.substring(3, 5);
}
@Test
public void appendString() throws Exception {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append("D");
assertThat(bc.toString(), equalTo("BC"));
assertThat(appended.toString(), equalTo("BCD"));
}
@Test
public void appendBytes() throws Exception {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append(new byte[] { 68 });
assertThat(bc.toString(), equalTo("BC"));
assertThat(appended.toString(), equalTo("BCD"));
}
@Test
public void hashCodeAndEquals() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 });
AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 })
.substring(1, 3);
AsciiBytes bc_string = new AsciiBytes("BC");
assertThat(bc.hashCode(), equalTo(bc.hashCode()));
assertThat(bc.hashCode(), equalTo(bc_substring.hashCode()));
assertThat(bc.hashCode(), equalTo(bc_string.hashCode()));
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)));
}
}

@ -33,10 +33,9 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry; import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.ExplodedArchive;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
@ -131,8 +130,8 @@ public class ExplodedArchiveTests {
Archive filteredArchive = this.archive Archive filteredArchive = this.archive
.getFilteredArchive(new Archive.EntryRenameFilter() { .getFilteredArchive(new Archive.EntryRenameFilter() {
@Override @Override
public String apply(String entryName, Entry entry) { public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
if (entryName.equals("1.dat")) { if (entryName.toString().equals("1.dat")) {
return entryName; return entryName;
} }
return null; return null;
@ -149,7 +148,7 @@ public class ExplodedArchiveTests {
private Map<String, Archive.Entry> getEntriesMap(Archive archive) { private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>(); Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
for (Archive.Entry entry : archive.getEntries()) { for (Archive.Entry entry : archive.getEntries()) {
entries.put(entry.getName(), entry); entries.put(entry.getName().toString(), entry);
} }
return entries; return entries;
} }

@ -25,9 +25,8 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.archive.Archive.Entry; import org.springframework.boot.loader.archive.Archive.Entry;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -86,8 +85,8 @@ public class JarFileArchiveTests {
Archive filteredArchive = this.archive Archive filteredArchive = this.archive
.getFilteredArchive(new Archive.EntryRenameFilter() { .getFilteredArchive(new Archive.EntryRenameFilter() {
@Override @Override
public String apply(String entryName, Entry entry) { public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
if (entryName.equals("1.dat")) { if (entryName.toString().equals("1.dat")) {
return entryName; return entryName;
} }
return null; return null;
@ -100,7 +99,7 @@ public class JarFileArchiveTests {
private Map<String, Archive.Entry> getEntriesMap(Archive archive) { private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>(); Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
for (Archive.Entry entry : archive.getEntries()) { for (Archive.Entry entry : archive.getEntries()) {
entries.put(entry.getName(), entry); entries.put(entry.getName().toString(), entry);
} }
return entries; return entries;
} }

@ -17,6 +17,7 @@
package org.springframework.boot.loader.data; package org.springframework.boot.loader.data;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -33,7 +34,8 @@ public class ByteArrayRandomAccessDataTest {
public void testGetInputStream() throws Exception { public void testGetInputStream() throws Exception {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 }; byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes); RandomAccessData data = new ByteArrayRandomAccessData(bytes);
assertThat(FileCopyUtils.copyToByteArray(data.getInputStream()), equalTo(bytes)); assertThat(FileCopyUtils.copyToByteArray(data
.getInputStream(ResourceAccess.PER_READ)), equalTo(bytes));
assertThat(data.getSize(), equalTo((long) bytes.length)); assertThat(data.getSize(), equalTo((long) bytes.length));
} }
@ -42,8 +44,8 @@ public class ByteArrayRandomAccessDataTest {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 }; byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes); RandomAccessData data = new ByteArrayRandomAccessData(bytes);
data = data.getSubsection(1, 4).getSubsection(1, 2); data = data.getSubsection(1, 4).getSubsection(1, 2);
assertThat(FileCopyUtils.copyToByteArray(data.getInputStream()), assertThat(FileCopyUtils.copyToByteArray(data
equalTo(new byte[] { 2, 3 })); .getInputStream(ResourceAccess.PER_READ)), equalTo(new byte[] { 2, 3 }));
assertThat(data.getSize(), equalTo(2L)); assertThat(data.getSize(), equalTo(2L));
} }
} }

@ -16,9 +16,6 @@
package org.springframework.boot.loader.data; package org.springframework.boot.loader.data;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
@ -40,8 +37,10 @@ import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.ByteArrayStartsWith; import org.springframework.boot.loader.ByteArrayStartsWith;
import org.springframework.boot.loader.data.RandomAccessData; import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/** /**
* Tests for {@link RandomAccessDataFile}. * Tests for {@link RandomAccessDataFile}.
@ -72,73 +71,73 @@ public class RandomAccessDataFileTests {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
this.tempFile = temporaryFolder.newFile(); this.tempFile = this.temporaryFolder.newFile();
FileOutputStream outputStream = new FileOutputStream(tempFile); FileOutputStream outputStream = new FileOutputStream(this.tempFile);
outputStream.write(BYTES); outputStream.write(BYTES);
outputStream.close(); outputStream.close();
this.file = new RandomAccessDataFile(tempFile); this.file = new RandomAccessDataFile(this.tempFile);
this.inputStream = file.getInputStream(); this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
} }
@After @After
public void cleanup() throws Exception { public void cleanup() throws Exception {
inputStream.close(); this.inputStream.close();
file.close(); this.file.close();
} }
@Test @Test
public void fileNotNull() throws Exception { public void fileNotNull() throws Exception {
thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
thrown.equals("File must not be null"); this.thrown.equals("File must not be null");
new RandomAccessDataFile(null); new RandomAccessDataFile(null);
} }
@Test @Test
public void fileExists() throws Exception { public void fileExists() throws Exception {
thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
thrown.equals("File must exist"); this.thrown.equals("File must exist");
new RandomAccessDataFile(new File("/does/not/exist")); new RandomAccessDataFile(new File("/does/not/exist"));
} }
@Test @Test
public void fileNotNullWithConcurrentReads() throws Exception { public void fileNotNullWithConcurrentReads() throws Exception {
thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
thrown.equals("File must not be null"); this.thrown.equals("File must not be null");
new RandomAccessDataFile(null, 1); new RandomAccessDataFile(null, 1);
} }
@Test @Test
public void fileExistsWithConcurrentReads() throws Exception { public void fileExistsWithConcurrentReads() throws Exception {
thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
thrown.equals("File must exist"); this.thrown.equals("File must exist");
new RandomAccessDataFile(new File("/does/not/exist"), 1); new RandomAccessDataFile(new File("/does/not/exist"), 1);
} }
@Test @Test
public void inputStreamRead() throws Exception { public void inputStreamRead() throws Exception {
for (int i = 0; i <= 255; i++) { for (int i = 0; i <= 255; i++) {
assertThat(inputStream.read(), equalTo(i)); assertThat(this.inputStream.read(), equalTo(i));
} }
} }
@Test @Test
public void inputStreamReadNullBytes() throws Exception { public void inputStreamReadNullBytes() throws Exception {
thrown.expect(NullPointerException.class); this.thrown.expect(NullPointerException.class);
thrown.expectMessage("Bytes must not be null"); this.thrown.expectMessage("Bytes must not be null");
inputStream.read(null); this.inputStream.read(null);
} }
@Test @Test
public void intputStreamReadNullBytesWithOffset() throws Exception { public void intputStreamReadNullBytesWithOffset() throws Exception {
thrown.expect(NullPointerException.class); this.thrown.expect(NullPointerException.class);
thrown.expectMessage("Bytes must not be null"); this.thrown.expectMessage("Bytes must not be null");
inputStream.read(null, 0, 1); this.inputStream.read(null, 0, 1);
} }
@Test @Test
public void inputStreamReadBytes() throws Exception { public void inputStreamReadBytes() throws Exception {
byte[] b = new byte[256]; byte[] b = new byte[256];
int amountRead = inputStream.read(b); int amountRead = this.inputStream.read(b);
assertThat(b, equalTo(BYTES)); assertThat(b, equalTo(BYTES));
assertThat(amountRead, equalTo(256)); assertThat(amountRead, equalTo(256));
} }
@ -146,8 +145,8 @@ public class RandomAccessDataFileTests {
@Test @Test
public void inputSteamReadOffsetBytes() throws Exception { public void inputSteamReadOffsetBytes() throws Exception {
byte[] b = new byte[7]; byte[] b = new byte[7];
inputStream.skip(1); this.inputStream.skip(1);
int amountRead = inputStream.read(b, 2, 3); int amountRead = this.inputStream.read(b, 2, 3);
assertThat(b, equalTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 })); assertThat(b, equalTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 }));
assertThat(amountRead, equalTo(3)); assertThat(amountRead, equalTo(3));
} }
@ -155,91 +154,91 @@ public class RandomAccessDataFileTests {
@Test @Test
public void inputStreamReadMoreBytesThanAvailable() throws Exception { public void inputStreamReadMoreBytesThanAvailable() throws Exception {
byte[] b = new byte[257]; byte[] b = new byte[257];
int amountRead = inputStream.read(b); int amountRead = this.inputStream.read(b);
assertThat(b, startsWith(BYTES)); assertThat(b, startsWith(BYTES));
assertThat(amountRead, equalTo(256)); assertThat(amountRead, equalTo(256));
} }
@Test @Test
public void inputStreamReadPastEnd() throws Exception { public void inputStreamReadPastEnd() throws Exception {
inputStream.skip(255); this.inputStream.skip(255);
assertThat(inputStream.read(), equalTo(0xFF)); assertThat(this.inputStream.read(), equalTo(0xFF));
assertThat(inputStream.read(), equalTo(-1)); assertThat(this.inputStream.read(), equalTo(-1));
assertThat(inputStream.read(), equalTo(-1)); assertThat(this.inputStream.read(), equalTo(-1));
} }
@Test @Test
public void inputStreamReadZeroLength() throws Exception { public void inputStreamReadZeroLength() throws Exception {
byte[] b = new byte[] { 0x0F }; byte[] b = new byte[] { 0x0F };
int amountRead = inputStream.read(b, 0, 0); int amountRead = this.inputStream.read(b, 0, 0);
assertThat(b, equalTo(new byte[] { 0x0F })); assertThat(b, equalTo(new byte[] { 0x0F }));
assertThat(amountRead, equalTo(0)); assertThat(amountRead, equalTo(0));
assertThat(inputStream.read(), equalTo(0)); assertThat(this.inputStream.read(), equalTo(0));
} }
@Test @Test
public void inputStreamSkip() throws Exception { public void inputStreamSkip() throws Exception {
long amountSkipped = inputStream.skip(4); long amountSkipped = this.inputStream.skip(4);
assertThat(inputStream.read(), equalTo(4)); assertThat(this.inputStream.read(), equalTo(4));
assertThat(amountSkipped, equalTo(4L)); assertThat(amountSkipped, equalTo(4L));
} }
@Test @Test
public void inputStreamSkipMoreThanAvailable() throws Exception { public void inputStreamSkipMoreThanAvailable() throws Exception {
long amountSkipped = inputStream.skip(257); long amountSkipped = this.inputStream.skip(257);
assertThat(inputStream.read(), equalTo(-1)); assertThat(this.inputStream.read(), equalTo(-1));
assertThat(amountSkipped, equalTo(256L)); assertThat(amountSkipped, equalTo(256L));
} }
@Test @Test
public void inputStreamSkipPastEnd() throws Exception { public void inputStreamSkipPastEnd() throws Exception {
inputStream.skip(256); this.inputStream.skip(256);
long amountSkipped = inputStream.skip(1); long amountSkipped = this.inputStream.skip(1);
assertThat(amountSkipped, equalTo(0L)); assertThat(amountSkipped, equalTo(0L));
} }
@Test @Test
public void subsectionNegativeOffset() throws Exception { public void subsectionNegativeOffset() throws Exception {
thrown.expect(IndexOutOfBoundsException.class); this.thrown.expect(IndexOutOfBoundsException.class);
file.getSubsection(-1, 1); this.file.getSubsection(-1, 1);
} }
@Test @Test
public void subsectionNegativeLength() throws Exception { public void subsectionNegativeLength() throws Exception {
thrown.expect(IndexOutOfBoundsException.class); this.thrown.expect(IndexOutOfBoundsException.class);
file.getSubsection(0, -1); this.file.getSubsection(0, -1);
} }
@Test @Test
public void subsectionZeroLength() throws Exception { public void subsectionZeroLength() throws Exception {
RandomAccessData subsection = file.getSubsection(0, 0); RandomAccessData subsection = this.file.getSubsection(0, 0);
assertThat(subsection.getInputStream().read(), equalTo(-1)); assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read(), equalTo(-1));
} }
@Test @Test
public void subsectionTooBig() throws Exception { public void subsectionTooBig() throws Exception {
file.getSubsection(0, 256); this.file.getSubsection(0, 256);
thrown.expect(IndexOutOfBoundsException.class); this.thrown.expect(IndexOutOfBoundsException.class);
file.getSubsection(0, 257); this.file.getSubsection(0, 257);
} }
@Test @Test
public void subsectionTooBigWithOffset() throws Exception { public void subsectionTooBigWithOffset() throws Exception {
file.getSubsection(1, 255); this.file.getSubsection(1, 255);
thrown.expect(IndexOutOfBoundsException.class); this.thrown.expect(IndexOutOfBoundsException.class);
file.getSubsection(1, 256); this.file.getSubsection(1, 256);
} }
@Test @Test
public void subsection() throws Exception { public void subsection() throws Exception {
RandomAccessData subsection = file.getSubsection(1, 1); RandomAccessData subsection = this.file.getSubsection(1, 1);
assertThat(subsection.getInputStream().read(), equalTo(1)); assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read(), equalTo(1));
} }
@Test @Test
public void inputStreamReadPastSubsection() throws Exception { public void inputStreamReadPastSubsection() throws Exception {
RandomAccessData subsection = file.getSubsection(1, 2); RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(); InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
assertThat(inputStream.read(), equalTo(1)); assertThat(inputStream.read(), equalTo(1));
assertThat(inputStream.read(), equalTo(2)); assertThat(inputStream.read(), equalTo(2));
assertThat(inputStream.read(), equalTo(-1)); assertThat(inputStream.read(), equalTo(-1));
@ -247,8 +246,8 @@ public class RandomAccessDataFileTests {
@Test @Test
public void inputStreamReadBytesPastSubsection() throws Exception { public void inputStreamReadBytesPastSubsection() throws Exception {
RandomAccessData subsection = file.getSubsection(1, 2); RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(); InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
byte[] b = new byte[3]; byte[] b = new byte[3];
int amountRead = inputStream.read(b); int amountRead = inputStream.read(b);
assertThat(b, equalTo(new byte[] { 1, 2, 0 })); assertThat(b, equalTo(new byte[] { 1, 2, 0 }));
@ -257,20 +256,20 @@ public class RandomAccessDataFileTests {
@Test @Test
public void inputStreamSkipPastSubsection() throws Exception { public void inputStreamSkipPastSubsection() throws Exception {
RandomAccessData subsection = file.getSubsection(1, 2); RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(); InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
assertThat(inputStream.skip(3), equalTo(2L)); assertThat(inputStream.skip(3), equalTo(2L));
assertThat(inputStream.read(), equalTo(-1)); assertThat(inputStream.read(), equalTo(-1));
} }
@Test @Test
public void inputStreamSkipNegative() throws Exception { public void inputStreamSkipNegative() throws Exception {
assertThat(inputStream.skip(-1), equalTo(0L)); assertThat(this.inputStream.skip(-1), equalTo(0L));
} }
@Test @Test
public void getFile() throws Exception { public void getFile() throws Exception {
assertThat(file.getFile(), equalTo(tempFile)); assertThat(this.file.getFile(), equalTo(this.tempFile));
} }
@Test @Test
@ -282,8 +281,9 @@ public class RandomAccessDataFileTests {
@Override @Override
public Boolean call() throws Exception { public Boolean call() throws Exception {
InputStream subsectionInputStream = file.getSubsection(0, 256) InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
.getInputStream(); .getSubsection(0, 256)
.getInputStream(ResourceAccess.PER_READ);
byte[] b = new byte[256]; byte[] b = new byte[256];
subsectionInputStream.read(b); subsectionInputStream.read(b);
return Arrays.equals(b, BYTES); return Arrays.equals(b, BYTES);
@ -297,11 +297,11 @@ public class RandomAccessDataFileTests {
@Test @Test
public void close() throws Exception { public void close() throws Exception {
file.getInputStream().read(); this.file.getInputStream(ResourceAccess.PER_READ).read();
file.close(); this.file.close();
Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool"); Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
filePoolField.setAccessible(true); filePoolField.setAccessible(true);
Object filePool = filePoolField.get(file); Object filePool = filePoolField.get(this.file);
Field filesField = filePool.getClass().getDeclaredField("files"); Field filesField = filePool.getClass().getDeclaredField("files");
filesField.setAccessible(true); filesField.setAccessible(true);
Queue<?> queue = (Queue<?>) filesField.get(filePool); Queue<?> queue = (Queue<?>) filesField.get(filePool);

@ -1,95 +0,0 @@
/*
* Copyright 2012-2013 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 static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import org.springframework.boot.loader.jar.RandomAccessDataJarEntry;
import org.springframework.boot.loader.jar.RandomAccessDataJarInputStream;
/**
* Tests for {@link RandomAccessDataJarInputStream}.
*
* @author Phillip Webb
*/
public class RandomAccessDataJarInputStreamTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File file;
@Before
public void setup() throws Exception {
this.file = temporaryFolder.newFile();
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(file));
try {
writeDataEntry(zipOutputStream, "a", new byte[10]);
writeDataEntry(zipOutputStream, "b", new byte[20]);
}
finally {
zipOutputStream.close();
}
}
private void writeDataEntry(ZipOutputStream zipOutputStream, String name, byte[] data)
throws IOException {
ZipEntry entry = new ZipEntry(name);
entry.setMethod(ZipEntry.STORED);
entry.setSize(data.length);
entry.setCompressedSize(data.length);
CRC32 crc32 = new CRC32();
crc32.update(data);
entry.setCrc(crc32.getValue());
zipOutputStream.putNextEntry(entry);
zipOutputStream.write(data);
zipOutputStream.closeEntry();
}
@Test
public void entryData() throws Exception {
RandomAccessDataJarInputStream z = new RandomAccessDataJarInputStream(
new RandomAccessDataFile(file));
try {
RandomAccessDataJarEntry entry1 = z.getNextEntry();
RandomAccessDataJarEntry entry2 = z.getNextEntry();
assertThat(entry1.getName(), equalTo("a"));
assertThat(entry1.getData().getSize(), equalTo(10L));
assertThat(entry2.getName(), equalTo("b"));
assertThat(entry2.getData().getSize(), equalTo(20L));
assertThat(z.getNextEntry(), nullValue());
}
finally {
z.close();
}
}
}

@ -20,11 +20,9 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -33,6 +31,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessDataFile; import org.springframework.boot.loader.data.RandomAccessDataFile;
@ -42,12 +41,13 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
/** /**
* Tests for {@link RandomAccessJarFile}. * Tests for {@link JarFile}.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
@ -61,27 +61,18 @@ public class RandomAccessJarFileTests {
private File rootJarFile; private File rootJarFile;
private RandomAccessJarFile jarFile; private JarFile jarFile;
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
this.rootJarFile = this.temporaryFolder.newFile(); this.rootJarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.rootJarFile); TestJarCreator.createTestJar(this.rootJarFile);
this.jarFile = new RandomAccessJarFile(this.rootJarFile); this.jarFile = new JarFile(this.rootJarFile);
} }
@Test @Test
public void createFromFile() throws Exception { public void createFromFile() throws Exception {
RandomAccessJarFile jarFile = new RandomAccessJarFile(this.rootJarFile); JarFile jarFile = new JarFile(this.rootJarFile);
assertThat(jarFile.getName(), notNullValue(String.class));
jarFile.close();
}
@Test
public void createFromRandomAccessDataFile() throws Exception {
RandomAccessDataFile randomAccessDataFile = new RandomAccessDataFile(
this.rootJarFile, 1);
RandomAccessJarFile jarFile = new RandomAccessJarFile(randomAccessDataFile);
assertThat(jarFile.getName(), notNullValue(String.class)); assertThat(jarFile.getName(), notNullValue(String.class));
jarFile.close(); jarFile.close();
} }
@ -101,7 +92,7 @@ public class RandomAccessJarFileTests {
@Test @Test
public void getEntries() throws Exception { public void getEntries() throws Exception {
Enumeration<JarEntry> entries = this.jarFile.entries(); Enumeration<java.util.jar.JarEntry> entries = this.jarFile.entries();
assertThat(entries.nextElement().getName(), equalTo("META-INF/")); assertThat(entries.nextElement().getName(), equalTo("META-INF/"));
assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF")); assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF"));
assertThat(entries.nextElement().getName(), equalTo("1.dat")); assertThat(entries.nextElement().getName(), equalTo("1.dat"));
@ -114,7 +105,7 @@ public class RandomAccessJarFileTests {
@Test @Test
public void getJarEntry() throws Exception { public void getJarEntry() throws Exception {
JarEntry entry = this.jarFile.getJarEntry("1.dat"); java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat");
assertThat(entry, notNullValue(ZipEntry.class)); assertThat(entry, notNullValue(ZipEntry.class));
assertThat(entry.getName(), equalTo("1.dat")); assertThat(entry.getName(), equalTo("1.dat"));
} }
@ -143,7 +134,7 @@ public class RandomAccessJarFileTests {
public void close() throws Exception { public void close() throws Exception {
RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile( RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(
this.rootJarFile, 1)); this.rootJarFile, 1));
RandomAccessJarFile jarFile = new RandomAccessJarFile(randomAccessDataFile); JarFile jarFile = new JarFile(randomAccessDataFile);
jarFile.close(); jarFile.close();
verify(randomAccessDataFile).close(); verify(randomAccessDataFile).close();
} }
@ -154,7 +145,7 @@ public class RandomAccessJarFileTests {
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
+ "!/")); + "!/"));
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile)); assertThat(jarURLConnection.getJarFile(), sameInstance(this.jarFile));
assertThat(jarURLConnection.getJarEntry(), nullValue()); assertThat(jarURLConnection.getJarEntry(), nullValue());
assertThat(jarURLConnection.getContentLength(), greaterThan(1)); assertThat(jarURLConnection.getContentLength(), greaterThan(1));
assertThat(jarURLConnection.getContent(), sameInstance((Object) this.jarFile)); assertThat(jarURLConnection.getContent(), sameInstance((Object) this.jarFile));
@ -167,7 +158,7 @@ public class RandomAccessJarFileTests {
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
+ "!/1.dat")); + "!/1.dat"));
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile)); assertThat(jarURLConnection.getJarFile(), sameInstance(this.jarFile));
assertThat(jarURLConnection.getJarEntry(), assertThat(jarURLConnection.getJarEntry(),
sameInstance(this.jarFile.getJarEntry("1.dat"))); sameInstance(this.jarFile.getJarEntry("1.dat")));
assertThat(jarURLConnection.getContentLength(), equalTo(1)); assertThat(jarURLConnection.getContentLength(), equalTo(1));
@ -203,10 +194,10 @@ public class RandomAccessJarFileTests {
@Test @Test
public void getNestedJarFile() throws Exception { public void getNestedJarFile() throws Exception {
RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
.getEntry("nested.jar")); .getEntry("nested.jar"));
Enumeration<JarEntry> entries = nestedJarFile.entries(); Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName(), equalTo("META-INF/")); assertThat(entries.nextElement().getName(), equalTo("META-INF/"));
assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF")); assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF"));
assertThat(entries.nextElement().getName(), equalTo("3.dat")); assertThat(entries.nextElement().getName(), equalTo("3.dat"));
@ -222,15 +213,15 @@ public class RandomAccessJarFileTests {
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
+ "!/nested.jar!/")); + "!/nested.jar!/"));
assertThat(((JarURLConnection) url.openConnection()).getJarFile(), assertThat(((JarURLConnection) url.openConnection()).getJarFile(),
sameInstance((JarFile) nestedJarFile)); sameInstance(nestedJarFile));
} }
@Test @Test
public void getNestedJarDirectory() throws Exception { public void getNestedJarDirectory() throws Exception {
RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile JarFile nestedJarFile = this.jarFile
.getEntry("d/")); .getNestedJarFile(this.jarFile.getEntry("d/"));
Enumeration<JarEntry> entries = nestedJarFile.entries(); Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName(), equalTo("9.dat")); assertThat(entries.nextElement().getName(), equalTo("9.dat"));
assertThat(entries.hasMoreElements(), equalTo(false)); assertThat(entries.hasMoreElements(), equalTo(false));
@ -243,7 +234,7 @@ public class RandomAccessJarFileTests {
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
+ "!/d!/")); + "!/d!/"));
assertThat(((JarURLConnection) url.openConnection()).getJarFile(), assertThat(((JarURLConnection) url.openConnection()).getJarFile(),
sameInstance((JarFile) nestedJarFile)); sameInstance(nestedJarFile));
} }
@Test @Test
@ -263,17 +254,16 @@ public class RandomAccessJarFileTests {
@Test @Test
public void getFilteredJarFile() throws Exception { public void getFilteredJarFile() throws Exception {
RandomAccessJarFile filteredJarFile = this.jarFile JarFile filteredJarFile = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
.getFilteredJarFile(new JarEntryFilter() { @Override
@Override public AsciiBytes apply(AsciiBytes entryName, JarEntryData entry) {
public String apply(String entryName, JarEntry entry) { if (entryName.toString().equals("1.dat")) {
if (entryName.equals("1.dat")) { return new AsciiBytes("x.dat");
return "x.dat"; }
} return null;
return null; }
} });
}); Enumeration<java.util.jar.JarEntry> entries = filteredJarFile.entries();
Enumeration<JarEntry> entries = filteredJarFile.entries();
assertThat(entries.nextElement().getName(), equalTo("x.dat")); assertThat(entries.nextElement().getName(), equalTo("x.dat"));
assertThat(entries.hasMoreElements(), equalTo(false)); assertThat(entries.hasMoreElements(), equalTo(false));
@ -289,4 +279,31 @@ public class RandomAccessJarFileTests {
assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar")) assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))
.toString(), equalTo(this.rootJarFile.getPath() + "!/nested.jar")); .toString(), equalTo(this.rootJarFile.getPath() + "!/nested.jar"));
} }
@Test
public void verifySignedJar() throws Exception {
String classpath = System.getProperty("java.class.path");
String[] entries = classpath.split(System.getProperty("path.separator"));
String signedJarFile = null;
for (String entry : entries) {
if (entry.contains("bcprov")) {
signedJarFile = entry;
}
}
assertNotNull(signedJarFile);
java.util.jar.JarFile jarFile = new JarFile(new File(signedJarFile));
jarFile.getManifest();
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
InputStream inputStream = jarFile.getInputStream(jarEntry);
inputStream.skip(Long.MAX_VALUE);
inputStream.close();
if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory()
&& !jarEntry.getName().endsWith("TigerDigest.class")) {
assertNotNull("Missing cert " + jarEntry.getName(),
jarEntry.getCertificates());
}
}
}
} }

Loading…
Cancel
Save