mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-09-03 04:26:12 +08:00
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.
This commit is contained in:
parent
6a6159f106
commit
d2678e08de
@ -12,6 +12,26 @@
|
||||
<properties>
|
||||
<main.basedir>${basedir}/../..</main.basedir>
|
||||
</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>
|
||||
<profile>
|
||||
<id>integration</id>
|
||||
@ -45,16 +65,4 @@
|
||||
</build>
|
||||
</profile>
|
||||
</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>
|
||||
|
@ -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 {
|
||||
|
||||
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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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<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 Map<String, Entry> entries = new LinkedHashMap<String, Entry>();
|
||||
private Map<AsciiBytes, Entry> entries = new LinkedHashMap<AsciiBytes, Entry>();
|
||||
|
||||
private Manifest manifest;
|
||||
|
||||
@ -62,7 +65,7 @@ public class ExplodedArchive extends Archive {
|
||||
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.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<String, Entry> filteredEntries = new LinkedHashMap<String, Archive.Entry>();
|
||||
for (Map.Entry<String, Entry> entry : this.entries.entrySet()) {
|
||||
String filteredName = filter.apply(entry.getKey(), entry.getValue());
|
||||
Map<AsciiBytes, Entry> filteredEntries = new LinkedHashMap<AsciiBytes, Archive.Entry>();
|
||||
for (Map.Entry<AsciiBytes, Entry> entry : this.entries.entrySet()) {
|
||||
AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue());
|
||||
if (filteredName != null) {
|
||||
filteredEntries.put(filteredName, new FileEntry(filteredName,
|
||||
((FileEntry) entry.getValue()).getFile()));
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<Entry> 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<Entry> jarFileEntries = new ArrayList<Entry>();
|
||||
Enumeration<JarEntry> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
import java.util.jar.JarEntry;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
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
|
||||
*/
|
||||
public class RandomAccessDataJarEntry extends JarEntry {
|
||||
class JarURLStreamHandler extends URLStreamHandler {
|
||||
|
||||
private RandomAccessData data;
|
||||
private JarFile jarFile;
|
||||
|
||||
/**
|
||||
* Create new {@link RandomAccessDataJarEntry} instance.
|
||||
* @param entry the underlying {@link JarEntry}
|
||||
* @param data the entry data
|
||||
*/
|
||||
public RandomAccessDataJarEntry(JarEntry entry, RandomAccessData data) {
|
||||
super(entry);
|
||||
this.data = data;
|
||||
public JarURLStreamHandler(JarFile jarFile) {
|
||||
this.jarFile = jarFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RandomAccessData} for this entry.
|
||||
* @return the entry data
|
||||
*/
|
||||
public RandomAccessData getData() {
|
||||
return this.data;
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new JarURLConnection(url, this.jarFile);
|
||||
}
|
||||
}
|
@ -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.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.boot.loader.AsciiBytes;
|
||||
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.ExplodedArchive;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
@ -131,8 +130,8 @@ public class ExplodedArchiveTests {
|
||||
Archive filteredArchive = this.archive
|
||||
.getFilteredArchive(new Archive.EntryRenameFilter() {
|
||||
@Override
|
||||
public String apply(String entryName, Entry entry) {
|
||||
if (entryName.equals("1.dat")) {
|
||||
public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
|
||||
if (entryName.toString().equals("1.dat")) {
|
||||
return entryName;
|
||||
}
|
||||
return null;
|
||||
@ -149,7 +148,7 @@ public class ExplodedArchiveTests {
|
||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
||||
for (Archive.Entry entry : archive.getEntries()) {
|
||||
entries.put(entry.getName(), entry);
|
||||
entries.put(entry.getName().toString(), entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
@ -25,9 +25,8 @@ import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.boot.loader.AsciiBytes;
|
||||
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 static org.hamcrest.Matchers.equalTo;
|
||||
@ -86,8 +85,8 @@ public class JarFileArchiveTests {
|
||||
Archive filteredArchive = this.archive
|
||||
.getFilteredArchive(new Archive.EntryRenameFilter() {
|
||||
@Override
|
||||
public String apply(String entryName, Entry entry) {
|
||||
if (entryName.equals("1.dat")) {
|
||||
public AsciiBytes apply(AsciiBytes entryName, Entry entry) {
|
||||
if (entryName.toString().equals("1.dat")) {
|
||||
return entryName;
|
||||
}
|
||||
return null;
|
||||
@ -100,7 +99,7 @@ public class JarFileArchiveTests {
|
||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
|
||||
for (Archive.Entry entry : archive.getEntries()) {
|
||||
entries.put(entry.getName(), entry);
|
||||
entries.put(entry.getName().toString(), entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package org.springframework.boot.loader.data;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
@ -33,7 +34,8 @@ public class ByteArrayRandomAccessDataTest {
|
||||
public void testGetInputStream() throws Exception {
|
||||
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
|
||||
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));
|
||||
}
|
||||
|
||||
@ -42,8 +44,8 @@ public class ByteArrayRandomAccessDataTest {
|
||||
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
|
||||
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
|
||||
data = data.getSubsection(1, 4).getSubsection(1, 2);
|
||||
assertThat(FileCopyUtils.copyToByteArray(data.getInputStream()),
|
||||
equalTo(new byte[] { 2, 3 }));
|
||||
assertThat(FileCopyUtils.copyToByteArray(data
|
||||
.getInputStream(ResourceAccess.PER_READ)), equalTo(new byte[] { 2, 3 }));
|
||||
assertThat(data.getSize(), equalTo(2L));
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
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.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
@ -40,8 +37,10 @@ import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.boot.loader.ByteArrayStartsWith;
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RandomAccessDataFile}.
|
||||
@ -72,73 +71,73 @@ public class RandomAccessDataFileTests {
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
this.tempFile = temporaryFolder.newFile();
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||
this.tempFile = this.temporaryFolder.newFile();
|
||||
FileOutputStream outputStream = new FileOutputStream(this.tempFile);
|
||||
outputStream.write(BYTES);
|
||||
outputStream.close();
|
||||
this.file = new RandomAccessDataFile(tempFile);
|
||||
this.inputStream = file.getInputStream();
|
||||
this.file = new RandomAccessDataFile(this.tempFile);
|
||||
this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() throws Exception {
|
||||
inputStream.close();
|
||||
file.close();
|
||||
this.inputStream.close();
|
||||
this.file.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileNotNull() throws Exception {
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.equals("File must not be null");
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.equals("File must not be null");
|
||||
new RandomAccessDataFile(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileExists() throws Exception {
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.equals("File must exist");
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.equals("File must exist");
|
||||
new RandomAccessDataFile(new File("/does/not/exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileNotNullWithConcurrentReads() throws Exception {
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.equals("File must not be null");
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.equals("File must not be null");
|
||||
new RandomAccessDataFile(null, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileExistsWithConcurrentReads() throws Exception {
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.equals("File must exist");
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.equals("File must exist");
|
||||
new RandomAccessDataFile(new File("/does/not/exist"), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamRead() throws Exception {
|
||||
for (int i = 0; i <= 255; i++) {
|
||||
assertThat(inputStream.read(), equalTo(i));
|
||||
assertThat(this.inputStream.read(), equalTo(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadNullBytes() throws Exception {
|
||||
thrown.expect(NullPointerException.class);
|
||||
thrown.expectMessage("Bytes must not be null");
|
||||
inputStream.read(null);
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Bytes must not be null");
|
||||
this.inputStream.read(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intputStreamReadNullBytesWithOffset() throws Exception {
|
||||
thrown.expect(NullPointerException.class);
|
||||
thrown.expectMessage("Bytes must not be null");
|
||||
inputStream.read(null, 0, 1);
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Bytes must not be null");
|
||||
this.inputStream.read(null, 0, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadBytes() throws Exception {
|
||||
byte[] b = new byte[256];
|
||||
int amountRead = inputStream.read(b);
|
||||
int amountRead = this.inputStream.read(b);
|
||||
assertThat(b, equalTo(BYTES));
|
||||
assertThat(amountRead, equalTo(256));
|
||||
}
|
||||
@ -146,8 +145,8 @@ public class RandomAccessDataFileTests {
|
||||
@Test
|
||||
public void inputSteamReadOffsetBytes() throws Exception {
|
||||
byte[] b = new byte[7];
|
||||
inputStream.skip(1);
|
||||
int amountRead = inputStream.read(b, 2, 3);
|
||||
this.inputStream.skip(1);
|
||||
int amountRead = this.inputStream.read(b, 2, 3);
|
||||
assertThat(b, equalTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 }));
|
||||
assertThat(amountRead, equalTo(3));
|
||||
}
|
||||
@ -155,91 +154,91 @@ public class RandomAccessDataFileTests {
|
||||
@Test
|
||||
public void inputStreamReadMoreBytesThanAvailable() throws Exception {
|
||||
byte[] b = new byte[257];
|
||||
int amountRead = inputStream.read(b);
|
||||
int amountRead = this.inputStream.read(b);
|
||||
assertThat(b, startsWith(BYTES));
|
||||
assertThat(amountRead, equalTo(256));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadPastEnd() throws Exception {
|
||||
inputStream.skip(255);
|
||||
assertThat(inputStream.read(), equalTo(0xFF));
|
||||
assertThat(inputStream.read(), equalTo(-1));
|
||||
assertThat(inputStream.read(), equalTo(-1));
|
||||
this.inputStream.skip(255);
|
||||
assertThat(this.inputStream.read(), equalTo(0xFF));
|
||||
assertThat(this.inputStream.read(), equalTo(-1));
|
||||
assertThat(this.inputStream.read(), equalTo(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadZeroLength() throws Exception {
|
||||
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(amountRead, equalTo(0));
|
||||
assertThat(inputStream.read(), equalTo(0));
|
||||
assertThat(this.inputStream.read(), equalTo(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamSkip() throws Exception {
|
||||
long amountSkipped = inputStream.skip(4);
|
||||
assertThat(inputStream.read(), equalTo(4));
|
||||
long amountSkipped = this.inputStream.skip(4);
|
||||
assertThat(this.inputStream.read(), equalTo(4));
|
||||
assertThat(amountSkipped, equalTo(4L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamSkipMoreThanAvailable() throws Exception {
|
||||
long amountSkipped = inputStream.skip(257);
|
||||
assertThat(inputStream.read(), equalTo(-1));
|
||||
long amountSkipped = this.inputStream.skip(257);
|
||||
assertThat(this.inputStream.read(), equalTo(-1));
|
||||
assertThat(amountSkipped, equalTo(256L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamSkipPastEnd() throws Exception {
|
||||
inputStream.skip(256);
|
||||
long amountSkipped = inputStream.skip(1);
|
||||
this.inputStream.skip(256);
|
||||
long amountSkipped = this.inputStream.skip(1);
|
||||
assertThat(amountSkipped, equalTo(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsectionNegativeOffset() throws Exception {
|
||||
thrown.expect(IndexOutOfBoundsException.class);
|
||||
file.getSubsection(-1, 1);
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
this.file.getSubsection(-1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsectionNegativeLength() throws Exception {
|
||||
thrown.expect(IndexOutOfBoundsException.class);
|
||||
file.getSubsection(0, -1);
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
this.file.getSubsection(0, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsectionZeroLength() throws Exception {
|
||||
RandomAccessData subsection = file.getSubsection(0, 0);
|
||||
assertThat(subsection.getInputStream().read(), equalTo(-1));
|
||||
RandomAccessData subsection = this.file.getSubsection(0, 0);
|
||||
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read(), equalTo(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsectionTooBig() throws Exception {
|
||||
file.getSubsection(0, 256);
|
||||
thrown.expect(IndexOutOfBoundsException.class);
|
||||
file.getSubsection(0, 257);
|
||||
this.file.getSubsection(0, 256);
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
this.file.getSubsection(0, 257);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsectionTooBigWithOffset() throws Exception {
|
||||
file.getSubsection(1, 255);
|
||||
thrown.expect(IndexOutOfBoundsException.class);
|
||||
file.getSubsection(1, 256);
|
||||
this.file.getSubsection(1, 255);
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
this.file.getSubsection(1, 256);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsection() throws Exception {
|
||||
RandomAccessData subsection = file.getSubsection(1, 1);
|
||||
assertThat(subsection.getInputStream().read(), equalTo(1));
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 1);
|
||||
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read(), equalTo(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
assertThat(inputStream.read(), equalTo(1));
|
||||
assertThat(inputStream.read(), equalTo(2));
|
||||
assertThat(inputStream.read(), equalTo(-1));
|
||||
@ -247,8 +246,8 @@ public class RandomAccessDataFileTests {
|
||||
|
||||
@Test
|
||||
public void inputStreamReadBytesPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
byte[] b = new byte[3];
|
||||
int amountRead = inputStream.read(b);
|
||||
assertThat(b, equalTo(new byte[] { 1, 2, 0 }));
|
||||
@ -257,20 +256,20 @@ public class RandomAccessDataFileTests {
|
||||
|
||||
@Test
|
||||
public void inputStreamSkipPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
assertThat(inputStream.skip(3), equalTo(2L));
|
||||
assertThat(inputStream.read(), equalTo(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamSkipNegative() throws Exception {
|
||||
assertThat(inputStream.skip(-1), equalTo(0L));
|
||||
assertThat(this.inputStream.skip(-1), equalTo(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFile() throws Exception {
|
||||
assertThat(file.getFile(), equalTo(tempFile));
|
||||
assertThat(this.file.getFile(), equalTo(this.tempFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -282,8 +281,9 @@ public class RandomAccessDataFileTests {
|
||||
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
InputStream subsectionInputStream = file.getSubsection(0, 256)
|
||||
.getInputStream();
|
||||
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
|
||||
.getSubsection(0, 256)
|
||||
.getInputStream(ResourceAccess.PER_READ);
|
||||
byte[] b = new byte[256];
|
||||
subsectionInputStream.read(b);
|
||||
return Arrays.equals(b, BYTES);
|
||||
@ -297,11 +297,11 @@ public class RandomAccessDataFileTests {
|
||||
|
||||
@Test
|
||||
public void close() throws Exception {
|
||||
file.getInputStream().read();
|
||||
file.close();
|
||||
this.file.getInputStream(ResourceAccess.PER_READ).read();
|
||||
this.file.close();
|
||||
Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
|
||||
filePoolField.setAccessible(true);
|
||||
Object filePool = filePoolField.get(file);
|
||||
Object filePool = filePoolField.get(this.file);
|
||||
Field filesField = filePool.getClass().getDeclaredField("files");
|
||||
filesField.setAccessible(true);
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
@ -33,6 +31,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.springframework.boot.loader.AsciiBytes;
|
||||
import org.springframework.boot.loader.TestJarCreator;
|
||||
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.nullValue;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link RandomAccessJarFile}.
|
||||
* Tests for {@link JarFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ -61,27 +61,18 @@ public class RandomAccessJarFileTests {
|
||||
|
||||
private File rootJarFile;
|
||||
|
||||
private RandomAccessJarFile jarFile;
|
||||
private JarFile jarFile;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
this.rootJarFile = this.temporaryFolder.newFile();
|
||||
TestJarCreator.createTestJar(this.rootJarFile);
|
||||
this.jarFile = new RandomAccessJarFile(this.rootJarFile);
|
||||
this.jarFile = new JarFile(this.rootJarFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromFile() throws Exception {
|
||||
RandomAccessJarFile jarFile = new RandomAccessJarFile(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);
|
||||
JarFile jarFile = new JarFile(this.rootJarFile);
|
||||
assertThat(jarFile.getName(), notNullValue(String.class));
|
||||
jarFile.close();
|
||||
}
|
||||
@ -101,7 +92,7 @@ public class RandomAccessJarFileTests {
|
||||
|
||||
@Test
|
||||
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/MANIFEST.MF"));
|
||||
assertThat(entries.nextElement().getName(), equalTo("1.dat"));
|
||||
@ -114,7 +105,7 @@ public class RandomAccessJarFileTests {
|
||||
|
||||
@Test
|
||||
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.getName(), equalTo("1.dat"));
|
||||
}
|
||||
@ -143,7 +134,7 @@ public class RandomAccessJarFileTests {
|
||||
public void close() throws Exception {
|
||||
RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(
|
||||
this.rootJarFile, 1));
|
||||
RandomAccessJarFile jarFile = new RandomAccessJarFile(randomAccessDataFile);
|
||||
JarFile jarFile = new JarFile(randomAccessDataFile);
|
||||
jarFile.close();
|
||||
verify(randomAccessDataFile).close();
|
||||
}
|
||||
@ -154,7 +145,7 @@ public class RandomAccessJarFileTests {
|
||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||
+ "!/"));
|
||||
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||
assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile));
|
||||
assertThat(jarURLConnection.getJarFile(), sameInstance(this.jarFile));
|
||||
assertThat(jarURLConnection.getJarEntry(), nullValue());
|
||||
assertThat(jarURLConnection.getContentLength(), greaterThan(1));
|
||||
assertThat(jarURLConnection.getContent(), sameInstance((Object) this.jarFile));
|
||||
@ -167,7 +158,7 @@ public class RandomAccessJarFileTests {
|
||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||
+ "!/1.dat"));
|
||||
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||
assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile));
|
||||
assertThat(jarURLConnection.getJarFile(), sameInstance(this.jarFile));
|
||||
assertThat(jarURLConnection.getJarEntry(),
|
||||
sameInstance(this.jarFile.getJarEntry("1.dat")));
|
||||
assertThat(jarURLConnection.getContentLength(), equalTo(1));
|
||||
@ -203,10 +194,10 @@ public class RandomAccessJarFileTests {
|
||||
|
||||
@Test
|
||||
public void getNestedJarFile() throws Exception {
|
||||
RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
|
||||
JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
|
||||
.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/MANIFEST.MF"));
|
||||
assertThat(entries.nextElement().getName(), equalTo("3.dat"));
|
||||
@ -222,15 +213,15 @@ public class RandomAccessJarFileTests {
|
||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||
+ "!/nested.jar!/"));
|
||||
assertThat(((JarURLConnection) url.openConnection()).getJarFile(),
|
||||
sameInstance((JarFile) nestedJarFile));
|
||||
sameInstance(nestedJarFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNestedJarDirectory() throws Exception {
|
||||
RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
|
||||
.getEntry("d/"));
|
||||
JarFile nestedJarFile = this.jarFile
|
||||
.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.hasMoreElements(), equalTo(false));
|
||||
|
||||
@ -243,7 +234,7 @@ public class RandomAccessJarFileTests {
|
||||
assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath()
|
||||
+ "!/d!/"));
|
||||
assertThat(((JarURLConnection) url.openConnection()).getJarFile(),
|
||||
sameInstance((JarFile) nestedJarFile));
|
||||
sameInstance(nestedJarFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -263,17 +254,16 @@ public class RandomAccessJarFileTests {
|
||||
|
||||
@Test
|
||||
public void getFilteredJarFile() throws Exception {
|
||||
RandomAccessJarFile filteredJarFile = this.jarFile
|
||||
.getFilteredJarFile(new JarEntryFilter() {
|
||||
@Override
|
||||
public String apply(String entryName, JarEntry entry) {
|
||||
if (entryName.equals("1.dat")) {
|
||||
return "x.dat";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
Enumeration<JarEntry> entries = filteredJarFile.entries();
|
||||
JarFile filteredJarFile = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
|
||||
@Override
|
||||
public AsciiBytes apply(AsciiBytes entryName, JarEntryData entry) {
|
||||
if (entryName.toString().equals("1.dat")) {
|
||||
return new AsciiBytes("x.dat");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
Enumeration<java.util.jar.JarEntry> entries = filteredJarFile.entries();
|
||||
assertThat(entries.nextElement().getName(), equalTo("x.dat"));
|
||||
assertThat(entries.hasMoreElements(), equalTo(false));
|
||||
|
||||
@ -289,4 +279,31 @@ public class RandomAccessJarFileTests {
|
||||
assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("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…
Reference in New Issue
Block a user