Commit d2678e08 authored by Phillip Webb's avatar Phillip Webb

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.
parent 6a6159f1
......@@ -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>
/*
* 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) {
......
/*
* 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;
}
}
/*
* 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);
}
}
/*
* 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();
}
}
/*
* 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);
}
/*
* 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);
}
}
/*
* 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;
}
}
}
/*
* 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);
}
}
}
/*
* 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));
}
}
/*
* 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());
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment