diff --git a/spring-boot-tools/spring-boot-loader/pom.xml b/spring-boot-tools/spring-boot-loader/pom.xml
index e1358aae0b..f5ae73e1e2 100644
--- a/spring-boot-tools/spring-boot-loader/pom.xml
+++ b/spring-boot-tools/spring-boot-loader/pom.xml
@@ -12,6 +12,26 @@
${basedir}/../..
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+
+ org.bouncycastle
+ bcprov-jdk16
+ 1.46
+ test
+
+
integration
@@ -45,16 +65,4 @@
-
-
- org.slf4j
- jcl-over-slf4j
- test
-
-
- ch.qos.logback
- logback-classic
- test
-
-
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java
new file mode 100644
index 0000000000..980619cc6b
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java
index 3df18043be..23f1ff021f 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java
@@ -28,9 +28,11 @@ import org.springframework.boot.loader.archive.Archive;
*/
public class JarLauncher extends ExecutableArchiveLauncher {
+ private static final AsciiBytes LIB = new AsciiBytes("lib/");
+
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
- return !entry.isDirectory() && entry.getName().startsWith("lib/");
+ return !entry.isDirectory() && entry.getName().startsWith(LIB);
}
@Override
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
index fa2de4e260..adf964d494 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
@@ -23,7 +23,7 @@ import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
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}.
@@ -161,14 +161,16 @@ public class LaunchedURLClassLoader extends URLClassLoader {
String path = name.replace('.', '/').concat(".class");
for (URL url : getURLs()) {
try {
- if (url.getContent() instanceof RandomAccessJarFile) {
- RandomAccessJarFile jarFile = (RandomAccessJarFile) url
- .getContent();
- if (jarFile.getManifest() != null
- && jarFile.getJarEntry(path) != null) {
+ if (url.getContent() instanceof JarFile) {
+ JarFile jarFile = (JarFile) url.getContent();
+ // Check the jar entry data before needlessly creating the
+ // manifest
+ if (jarFile.getJarEntryData(path) != null
+ && jarFile.getManifest() != null) {
definePackage(packageName, jarFile.getManifest(), url);
return null;
}
+
}
}
catch (IOException e) {
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java
index e39fe5d428..52233eaaf1 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java
@@ -421,10 +421,15 @@ public class PropertiesLauncher extends Launcher {
* classpath entries).
*/
private static final class ArchiveEntryFilter implements EntryFilter {
+
+ private static final AsciiBytes DOT_JAR = new AsciiBytes(".jar");
+
+ private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip");
+
@Override
public boolean matches(Entry entry) {
- return entry.isDirectory() || entry.getName().endsWith(".jar")
- || entry.getName().endsWith(".zip");
+ return entry.isDirectory() || entry.getName().endsWith(DOT_JAR)
+ || entry.getName().endsWith(DOT_ZIP);
}
}
@@ -433,11 +438,13 @@ public class PropertiesLauncher extends Launcher {
* (e.g. "lib/").
*/
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
- private final String prefix;
+
+ private final AsciiBytes prefix;
+
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
private PrefixMatchingArchiveFilter(String prefix) {
- this.prefix = prefix;
+ this.prefix = new AsciiBytes(prefix);
}
@Override
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java
index ebe7a672be..7b764a125e 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java
@@ -30,14 +30,25 @@ import org.springframework.boot.loader.archive.Archive;
*/
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
public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
- return entry.getName().equals("WEB-INF/classes/");
+ return entry.getName().equals(WEB_INF_CLASSES);
}
else {
- return entry.getName().startsWith("WEB-INF/lib/")
- || entry.getName().startsWith("WEB-INF/lib-provided/");
+ return entry.getName().startsWith(WEB_INF_LIB)
+ || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
}
}
@@ -55,8 +66,8 @@ public class WarLauncher extends ExecutableArchiveLauncher {
protected Archive getFilteredArchive() throws IOException {
return getArchive().getFilteredArchive(new Archive.EntryRenameFilter() {
@Override
- public String apply(String entryName, Archive.Entry entry) {
- if (entryName.startsWith("META-INF/") || entryName.startsWith("WEB-INF/")) {
+ public AsciiBytes apply(AsciiBytes entryName, Archive.Entry entry) {
+ if (entryName.startsWith(META_INF) || entryName.startsWith(WEB_INF)) {
return null;
}
return entryName;
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java
index 9a89fbc2f0..2608a3ba81 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.List;
import java.util.jar.Manifest;
+import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.Launcher;
/**
@@ -115,7 +116,7 @@ public abstract class Archive {
* Returns 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
* included.
*/
- String apply(String entryName, Entry entry);
+ AsciiBytes apply(AsciiBytes entryName, Entry entry);
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java
index 5b56def472..a1e8114bfc 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java
@@ -35,6 +35,8 @@ import java.util.Map;
import java.util.Set;
import java.util.jar.Manifest;
+import org.springframework.boot.loader.AsciiBytes;
+
/**
* {@link Archive} implementation backed by an exploded archive directory.
*
@@ -45,11 +47,12 @@ public class ExplodedArchive extends Archive {
private static final Set SKIPPED_NAMES = new HashSet(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 Map entries = new LinkedHashMap();
+ private Map entries = new LinkedHashMap();
private Manifest manifest;
@@ -62,7 +65,7 @@ public class ExplodedArchive extends Archive {
this.entries = Collections.unmodifiableMap(this.entries);
}
- private ExplodedArchive(File root, Map entries) {
+ private ExplodedArchive(File root, Map entries) {
this.root = root;
this.entries = Collections.unmodifiableMap(entries);
}
@@ -74,7 +77,8 @@ public class ExplodedArchive extends Archive {
if (file.isDirectory()) {
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()) {
for (File child : file.listFiles()) {
@@ -129,9 +133,9 @@ public class ExplodedArchive extends Archive {
@Override
public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException {
- Map filteredEntries = new LinkedHashMap();
- for (Map.Entry entry : this.entries.entrySet()) {
- String filteredName = filter.apply(entry.getKey(), entry.getValue());
+ Map filteredEntries = new LinkedHashMap();
+ for (Map.Entry entry : this.entries.entrySet()) {
+ AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue());
if (filteredName != null) {
filteredEntries.put(filteredName, new FileEntry(filteredName,
((FileEntry) entry.getValue()).getFile()));
@@ -142,10 +146,11 @@ public class ExplodedArchive extends Archive {
private class FileEntry implements Entry {
- private final String name;
+ private final AsciiBytes name;
+
private final File file;
- public FileEntry(String name, File file) {
+ public FileEntry(AsciiBytes name, File file) {
this.name = name;
this.file = file;
}
@@ -160,7 +165,7 @@ public class ExplodedArchive extends Archive {
}
@Override
- public String getName() {
+ public AsciiBytes getName() {
return this.name;
}
}
@@ -177,7 +182,7 @@ public class ExplodedArchive extends Archive {
protected URLConnection openConnection(URL url) throws IOException {
String name = url.getPath().substring(
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 FileNotFoundURLConnection(url, name);
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java
index cc4a84ac54..f670408e02 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java
@@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List;
import java.util.jar.Manifest;
+import org.springframework.boot.loader.AsciiBytes;
+
/**
* @author Dave Syer
*/
@@ -79,7 +81,7 @@ public class FilteredArchive extends Archive {
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
return this.parent.getFilteredArchive(new EntryRenameFilter() {
@Override
- public String apply(String entryName, Entry entry) {
+ public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
return FilteredArchive.this.filter.matches(entry) ? filter.apply(
entryName, entry) : null;
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
index 1e465a8684..009721bc87 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
@@ -23,35 +23,35 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
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.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
*/
public class JarFileArchive extends Archive {
- private final RandomAccessJarFile jarFile;
+ private final JarFile jarFile;
private final List entries;
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;
ArrayList jarFileEntries = new ArrayList();
- Enumeration entries = jarFile.entries();
- while (entries.hasMoreElements()) {
- jarFileEntries.add(new JarFileEntry(entries.nextElement()));
+ for (JarEntryData data : jarFile) {
+ jarFileEntries.add(new JarFileEntry(data));
}
this.entries = Collections.unmodifiableList(jarFileEntries);
}
@@ -83,20 +83,19 @@ public class JarFileArchive extends Archive {
}
protected Archive getNestedArchive(Entry entry) throws IOException {
- JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
- RandomAccessJarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
+ JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
+ JarFile jarFile = this.jarFile.getNestedJarFile(data);
return new JarFileArchive(jarFile);
}
@Override
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
- RandomAccessJarFile filteredJar = this.jarFile
- .getFilteredJarFile(new JarEntryFilter() {
- @Override
- public String apply(String name, JarEntry entry) {
- return filter.apply(name, new JarFileEntry(entry));
- }
- });
+ JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
+ @Override
+ public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
+ return filter.apply(name, new JarFileEntry(entryData));
+ }
+ });
return new JarFileArchive(filteredJar);
}
@@ -105,24 +104,24 @@ public class JarFileArchive extends Archive {
*/
private static class JarFileEntry implements Entry {
- private final JarEntry jarEntry;
+ private final JarEntryData entryData;
- public JarFileEntry(JarEntry jarEntry) {
- this.jarEntry = jarEntry;
+ public JarFileEntry(JarEntryData entryData) {
+ this.entryData = entryData;
}
- public JarEntry getJarEntry() {
- return this.jarEntry;
+ public JarEntryData getJarEntryData() {
+ return this.entryData;
}
@Override
public boolean isDirectory() {
- return this.jarEntry.isDirectory();
+ return this.entryData.isDirectory();
}
@Override
- public String getName() {
- return this.jarEntry.getName();
+ public AsciiBytes getName() {
+ return this.entryData.getName();
}
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java
index 7697c27288..94fbb3b97c 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java
@@ -43,7 +43,7 @@ public class ByteArrayRandomAccessData implements RandomAccessData {
}
@Override
- public InputStream getInputStream() {
+ public InputStream getInputStream(ResourceAccess access) {
return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length);
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java
index 2ab0eea7fd..7ae0e74750 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java
@@ -16,6 +16,7 @@
package org.springframework.boot.loader.data;
+import java.io.IOException;
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
* 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.
+ * @throws IOException
*/
- InputStream getInputStream();
+ InputStream getInputStream(ResourceAccess access) throws IOException;
/**
* Returns a new {@link RandomAccessData} for a specific subsection of this data.
@@ -47,4 +50,20 @@ public interface RandomAccessData {
*/
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
+ }
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java
index 6fd9e01677..d09d88b290 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java
@@ -33,7 +33,7 @@ public class RandomAccessDataFile implements RandomAccessData {
private static final int DEFAULT_CONCURRENT_READS = 4;
- private File file;
+ private final File file;
private final FilePool filePool;
@@ -78,7 +78,8 @@ public class RandomAccessDataFile implements RandomAccessData {
* @param offset the offset 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.offset = offset;
this.length = length;
@@ -93,8 +94,8 @@ public class RandomAccessDataFile implements RandomAccessData {
}
@Override
- public InputStream getInputStream() {
- return new DataInputStream();
+ public InputStream getInputStream(ResourceAccess access) throws IOException {
+ return new DataInputStream(access);
}
@Override
@@ -102,7 +103,8 @@ public class RandomAccessDataFile implements RandomAccessData {
if (offset < 0 || length < 0 || offset + length > this.length) {
throw new IndexOutOfBoundsException();
}
- return new RandomAccessDataFile(this.filePool, this.offset + offset, length);
+ return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset,
+ length);
}
@Override
@@ -120,7 +122,16 @@ public class RandomAccessDataFile implements RandomAccessData {
*/
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
public int read() throws IOException {
@@ -153,23 +164,29 @@ public class RandomAccessDataFile implements RandomAccessData {
if (len == 0) {
return 0;
}
- if (cap(len) <= 0) {
+ int cappedLen = cap(len);
+ if (cappedLen <= 0) {
return -1;
}
- RandomAccessFile file = RandomAccessDataFile.this.filePool.acquire();
- try {
+ RandomAccessFile file = this.file;
+ if (file == null) {
+ file = RandomAccessDataFile.this.filePool.acquire();
file.seek(RandomAccessDataFile.this.offset + this.position);
+ }
+ try {
if (b == null) {
int rtn = file.read();
moveOn(rtn == -1 ? 0 : 1);
return rtn;
}
else {
- return (int) moveOn(file.read(b, off, (int) cap(len)));
+ return (int) moveOn(file.read(b, off, cappedLen));
}
}
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)));
}
+ @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
* remaining.
* @param n the value to cap
* @return the capped value
*/
- private long cap(long n) {
- return Math.min(RandomAccessDataFile.this.length - this.position, n);
+ private int cap(long 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
* @return the amount moved
*/
- private long moveOn(long amount) {
+ private long moveOn(int amount) {
this.position += amount;
return amount;
}
@@ -237,16 +261,16 @@ public class RandomAccessDataFile implements RandomAccessData {
public void close() throws IOException {
try {
- this.available.acquire(size);
+ this.available.acquire(this.size);
try {
- RandomAccessFile file = files.poll();
+ RandomAccessFile file = this.files.poll();
while (file != null) {
file.close();
- file = files.poll();
+ file = this.files.poll();
}
}
finally {
- this.available.release(size);
+ this.available.release(this.size);
}
}
catch (InterruptedException ex) {
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java
new file mode 100644
index 0000000000..c9d348f150
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java
@@ -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;
+ }
+}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java
new file mode 100644
index 0000000000..b3c40eb599
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java
@@ -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 Zip File Format
+ */
+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);
+ }
+}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java
new file mode 100644
index 0000000000..f8145c412a
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java
@@ -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();
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java
new file mode 100644
index 0000000000..7c086cac3b
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java
@@ -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 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(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);
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java
index 43c5602b9e..53284716dd 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java
@@ -16,7 +16,7 @@
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.
@@ -27,12 +27,12 @@ public interface JarEntryFilter {
/**
* Apply the jar entry filter.
- * @param entryName the current entry name. This may be different that the original
- * entry name if a previous filter has been applied
- * @param entry the entry to filter
+ * @param name the current entry name. This may be different that the original entry
+ * name if a previous filter has been applied
+ * @param entryData the entry data to filter
* @return the new name of the entry or {@code null} if the entry should not be
* included.
*/
- String apply(String entryName, JarEntry entry);
+ AsciiBytes apply(AsciiBytes name, JarEntryData entryData);
}
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java
new file mode 100644
index 0000000000..ba7a08da8c
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java
@@ -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.
+ *
+ * - Jar entries can be {@link JarEntryFilter filtered} during construction and new
+ * filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
+ * existing files.
+ * - A nested {@link JarFile} can be
+ * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
+ * entry.
+ * - A nested {@link JarFile} can be
+ * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files
+ * (as long as their entry is not compressed).
+ * - Entry data can be accessed as {@link RandomAccessData}.
+ *
+ *
+ * @author Phillip Webb
+ */
+public class JarFile extends java.util.jar.JarFile implements Iterable {
+
+ 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 entries;
+
+ private SoftReference