diff --git a/spring-boot-tools/spring-boot-loader/pom.xml b/spring-boot-tools/spring-boot-loader/pom.xml index e1358aae0b..f5ae73e1e2 100644 --- a/spring-boot-tools/spring-boot-loader/pom.xml +++ b/spring-boot-tools/spring-boot-loader/pom.xml @@ -12,6 +12,26 @@ ${basedir}/../.. + + + + org.slf4j + jcl-over-slf4j + test + + + ch.qos.logback + logback-classic + test + + + + org.bouncycastle + bcprov-jdk16 + 1.46 + test + + integration @@ -45,16 +65,4 @@ - - - org.slf4j - jcl-over-slf4j - test - - - ch.qos.logback - logback-classic - test - - diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java new file mode 100644 index 0000000000..980619cc6b --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/AsciiBytes.java @@ -0,0 +1,181 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader; + +import java.nio.charset.Charset; + +/** + * Simple wrapper around a byte array that represents an ASCII. Used for performance + * reasons to save constructing Strings for ZIP data. + * + * @author Phillip Webb + */ +public final class AsciiBytes { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final int INITIAL_HASH = 7; + + private static final int MULTIPLIER = 31; + + private final byte[] bytes; + + private final int offset; + + private final int length; + + private String string; + + /** + * Create a new {@link AsciiBytes} from the specified String. + * @param string + */ + public AsciiBytes(String string) { + this(string.getBytes()); + this.string = string; + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the bytes + */ + public AsciiBytes(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the bytes + * @param offset the offset + * @param length the length + */ + public AsciiBytes(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || (offset + length) > bytes.length) { + throw new IndexOutOfBoundsException(); + } + this.bytes = bytes; + this.offset = offset; + this.length = length; + } + + public int length() { + return this.length; + } + + public boolean startsWith(AsciiBytes prefix) { + if (this == prefix) { + return true; + } + if (prefix.length > this.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) { + return false; + } + } + return true; + } + + public boolean endsWith(AsciiBytes postfix) { + if (this == postfix) { + return true; + } + if (postfix.length > this.length) { + return false; + } + for (int i = 0; i < postfix.length; i++) { + if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + + (postfix.length - 1) - i]) { + return false; + } + } + return true; + } + + public AsciiBytes substring(int beginIndex) { + return substring(beginIndex, this.length); + } + + public AsciiBytes substring(int beginIndex, int endIndex) { + int length = endIndex - beginIndex; + if (this.offset + length > this.length) { + throw new IndexOutOfBoundsException(); + } + return new AsciiBytes(this.bytes, this.offset + beginIndex, length); + } + + public AsciiBytes append(String string) { + if (string == null || string.length() == 0) { + return this; + } + return append(string.getBytes()); + } + + public AsciiBytes append(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return this; + } + byte[] combined = new byte[this.length + bytes.length]; + System.arraycopy(this.bytes, this.offset, combined, 0, this.length); + System.arraycopy(bytes, 0, combined, this.length, bytes.length); + return new AsciiBytes(combined); + } + + @Override + public String toString() { + if (this.string == null) { + this.string = new String(this.bytes, this.offset, this.length, UTF_8); + } + return this.string; + } + + @Override + public int hashCode() { + int hash = INITIAL_HASH; + for (int i = 0; i < this.length; i++) { + hash = MULTIPLIER * hash + this.bytes[this.offset + i]; + } + return hash; + + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj.getClass().equals(AsciiBytes.class)) { + AsciiBytes other = (AsciiBytes) obj; + if (this.length == other.length) { + for (int i = 0; i < this.length; i++) { + if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) { + return false; + } + } + return true; + } + } + return false; + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index 3df18043be..23f1ff021f 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -28,9 +28,11 @@ import org.springframework.boot.loader.archive.Archive; */ public class JarLauncher extends ExecutableArchiveLauncher { + private static final AsciiBytes LIB = new AsciiBytes("lib/"); + @Override protected boolean isNestedArchive(Archive.Entry entry) { - return !entry.isDirectory() && entry.getName().startsWith("lib/"); + return !entry.isDirectory() && entry.getName().startsWith(LIB); } @Override diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index fa2de4e260..adf964d494 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -23,7 +23,7 @@ import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.Enumeration; -import org.springframework.boot.loader.jar.RandomAccessJarFile; +import org.springframework.boot.loader.jar.JarFile; /** * {@link ClassLoader} used by the {@link Launcher}. @@ -161,14 +161,16 @@ public class LaunchedURLClassLoader extends URLClassLoader { String path = name.replace('.', '/').concat(".class"); for (URL url : getURLs()) { try { - if (url.getContent() instanceof RandomAccessJarFile) { - RandomAccessJarFile jarFile = (RandomAccessJarFile) url - .getContent(); - if (jarFile.getManifest() != null - && jarFile.getJarEntry(path) != null) { + if (url.getContent() instanceof JarFile) { + JarFile jarFile = (JarFile) url.getContent(); + // Check the jar entry data before needlessly creating the + // manifest + if (jarFile.getJarEntryData(path) != null + && jarFile.getManifest() != null) { definePackage(packageName, jarFile.getManifest(), url); return null; } + } } catch (IOException e) { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index e39fe5d428..52233eaaf1 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -421,10 +421,15 @@ public class PropertiesLauncher extends Launcher { * classpath entries). */ private static final class ArchiveEntryFilter implements EntryFilter { + + private static final AsciiBytes DOT_JAR = new AsciiBytes(".jar"); + + private static final AsciiBytes DOT_ZIP = new AsciiBytes(".zip"); + @Override public boolean matches(Entry entry) { - return entry.isDirectory() || entry.getName().endsWith(".jar") - || entry.getName().endsWith(".zip"); + return entry.isDirectory() || entry.getName().endsWith(DOT_JAR) + || entry.getName().endsWith(DOT_ZIP); } } @@ -433,11 +438,13 @@ public class PropertiesLauncher extends Launcher { * (e.g. "lib/"). */ private static final class PrefixMatchingArchiveFilter implements EntryFilter { - private final String prefix; + + private final AsciiBytes prefix; + private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); private PrefixMatchingArchiveFilter(String prefix) { - this.prefix = prefix; + this.prefix = new AsciiBytes(prefix); } @Override diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java index ebe7a672be..7b764a125e 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -30,14 +30,25 @@ import org.springframework.boot.loader.archive.Archive; */ public class WarLauncher extends ExecutableArchiveLauncher { + private static final AsciiBytes WEB_INF = new AsciiBytes("WEB-INF/"); + + private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); + + private static final AsciiBytes WEB_INF_CLASSES = WEB_INF.append("classes/"); + + private static final AsciiBytes WEB_INF_LIB = WEB_INF.append("lib/"); + + private static final AsciiBytes WEB_INF_LIB_PROVIDED = WEB_INF + .append("lib-provided/"); + @Override public boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { - return entry.getName().equals("WEB-INF/classes/"); + return entry.getName().equals(WEB_INF_CLASSES); } else { - return entry.getName().startsWith("WEB-INF/lib/") - || entry.getName().startsWith("WEB-INF/lib-provided/"); + return entry.getName().startsWith(WEB_INF_LIB) + || entry.getName().startsWith(WEB_INF_LIB_PROVIDED); } } @@ -55,8 +66,8 @@ public class WarLauncher extends ExecutableArchiveLauncher { protected Archive getFilteredArchive() throws IOException { return getArchive().getFilteredArchive(new Archive.EntryRenameFilter() { @Override - public String apply(String entryName, Archive.Entry entry) { - if (entryName.startsWith("META-INF/") || entryName.startsWith("WEB-INF/")) { + public AsciiBytes apply(AsciiBytes entryName, Archive.Entry entry) { + if (entryName.startsWith(META_INF) || entryName.startsWith(WEB_INF)) { return null; } return entryName; diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java index 9a89fbc2f0..2608a3ba81 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.List; import java.util.jar.Manifest; +import org.springframework.boot.loader.AsciiBytes; import org.springframework.boot.loader.Launcher; /** @@ -115,7 +116,7 @@ public abstract class Archive { * Returns the name of the entry * @return the name of the entry */ - String getName(); + AsciiBytes getName(); } @@ -146,7 +147,7 @@ public abstract class Archive { * @return the new name of the entry or {@code null} if the entry should not be * included. */ - String apply(String entryName, Entry entry); + AsciiBytes apply(AsciiBytes entryName, Entry entry); } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java index 5b56def472..a1e8114bfc 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -35,6 +35,8 @@ import java.util.Map; import java.util.Set; import java.util.jar.Manifest; +import org.springframework.boot.loader.AsciiBytes; + /** * {@link Archive} implementation backed by an exploded archive directory. * @@ -45,11 +47,12 @@ public class ExplodedArchive extends Archive { private static final Set SKIPPED_NAMES = new HashSet(Arrays.asList( ".", "..")); - private static final Object MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + private static final AsciiBytes MANIFEST_ENTRY_NAME = new AsciiBytes( + "META-INF/MANIFEST.MF"); private File root; - private Map entries = new LinkedHashMap(); + private Map entries = new LinkedHashMap(); private Manifest manifest; @@ -62,7 +65,7 @@ public class ExplodedArchive extends Archive { this.entries = Collections.unmodifiableMap(this.entries); } - private ExplodedArchive(File root, Map entries) { + private ExplodedArchive(File root, Map entries) { this.root = root; this.entries = Collections.unmodifiableMap(entries); } @@ -74,7 +77,8 @@ public class ExplodedArchive extends Archive { if (file.isDirectory()) { name += "/"; } - this.entries.put(name, new FileEntry(name, file)); + FileEntry entry = new FileEntry(new AsciiBytes(name), file); + this.entries.put(entry.getName(), entry); } if (file.isDirectory()) { for (File child : file.listFiles()) { @@ -129,9 +133,9 @@ public class ExplodedArchive extends Archive { @Override public Archive getFilteredArchive(EntryRenameFilter filter) throws IOException { - Map filteredEntries = new LinkedHashMap(); - for (Map.Entry entry : this.entries.entrySet()) { - String filteredName = filter.apply(entry.getKey(), entry.getValue()); + Map filteredEntries = new LinkedHashMap(); + for (Map.Entry entry : this.entries.entrySet()) { + AsciiBytes filteredName = filter.apply(entry.getKey(), entry.getValue()); if (filteredName != null) { filteredEntries.put(filteredName, new FileEntry(filteredName, ((FileEntry) entry.getValue()).getFile())); @@ -142,10 +146,11 @@ public class ExplodedArchive extends Archive { private class FileEntry implements Entry { - private final String name; + private final AsciiBytes name; + private final File file; - public FileEntry(String name, File file) { + public FileEntry(AsciiBytes name, File file) { this.name = name; this.file = file; } @@ -160,7 +165,7 @@ public class ExplodedArchive extends Archive { } @Override - public String getName() { + public AsciiBytes getName() { return this.name; } } @@ -177,7 +182,7 @@ public class ExplodedArchive extends Archive { protected URLConnection openConnection(URL url) throws IOException { String name = url.getPath().substring( ExplodedArchive.this.root.getAbsolutePath().length() + 1); - if (ExplodedArchive.this.entries.containsKey(name)) { + if (ExplodedArchive.this.entries.containsKey(new AsciiBytes(name))) { return new URL(url.toString()).openConnection(); } return new FileNotFoundURLConnection(url, name); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java index cc4a84ac54..f670408e02 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/FilteredArchive.java @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.List; import java.util.jar.Manifest; +import org.springframework.boot.loader.AsciiBytes; + /** * @author Dave Syer */ @@ -79,7 +81,7 @@ public class FilteredArchive extends Archive { public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { return this.parent.getFilteredArchive(new EntryRenameFilter() { @Override - public String apply(String entryName, Entry entry) { + public AsciiBytes apply(AsciiBytes entryName, Entry entry) { return FilteredArchive.this.filter.matches(entry) ? filter.apply( entryName, entry) : null; } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index 1e465a8684..009721bc87 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -23,35 +23,35 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.Manifest; +import org.springframework.boot.loader.AsciiBytes; +import org.springframework.boot.loader.jar.JarEntryData; import org.springframework.boot.loader.jar.JarEntryFilter; -import org.springframework.boot.loader.jar.RandomAccessJarFile; +import org.springframework.boot.loader.jar.JarFile; /** - * {@link Archive} implementation backed by a {@link RandomAccessJarFile}. + * {@link Archive} implementation backed by a {@link JarFile}. * * @author Phillip Webb */ public class JarFileArchive extends Archive { - private final RandomAccessJarFile jarFile; + private final JarFile jarFile; private final List entries; public JarFileArchive(File file) throws IOException { - this(new RandomAccessJarFile(file)); + this(new JarFile(file)); } - public JarFileArchive(RandomAccessJarFile jarFile) { + public JarFileArchive(JarFile jarFile) { this.jarFile = jarFile; ArrayList jarFileEntries = new ArrayList(); - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - jarFileEntries.add(new JarFileEntry(entries.nextElement())); + for (JarEntryData data : jarFile) { + jarFileEntries.add(new JarFileEntry(data)); } this.entries = Collections.unmodifiableList(jarFileEntries); } @@ -83,20 +83,19 @@ public class JarFileArchive extends Archive { } protected Archive getNestedArchive(Entry entry) throws IOException { - JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); - RandomAccessJarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + JarEntryData data = ((JarFileEntry) entry).getJarEntryData(); + JarFile jarFile = this.jarFile.getNestedJarFile(data); return new JarFileArchive(jarFile); } @Override public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { - RandomAccessJarFile filteredJar = this.jarFile - .getFilteredJarFile(new JarEntryFilter() { - @Override - public String apply(String name, JarEntry entry) { - return filter.apply(name, new JarFileEntry(entry)); - } - }); + JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() { + @Override + public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) { + return filter.apply(name, new JarFileEntry(entryData)); + } + }); return new JarFileArchive(filteredJar); } @@ -105,24 +104,24 @@ public class JarFileArchive extends Archive { */ private static class JarFileEntry implements Entry { - private final JarEntry jarEntry; + private final JarEntryData entryData; - public JarFileEntry(JarEntry jarEntry) { - this.jarEntry = jarEntry; + public JarFileEntry(JarEntryData entryData) { + this.entryData = entryData; } - public JarEntry getJarEntry() { - return this.jarEntry; + public JarEntryData getJarEntryData() { + return this.entryData; } @Override public boolean isDirectory() { - return this.jarEntry.isDirectory(); + return this.entryData.isDirectory(); } @Override - public String getName() { - return this.jarEntry.getName(); + public AsciiBytes getName() { + return this.entryData.getName(); } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java index 7697c27288..94fbb3b97c 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/ByteArrayRandomAccessData.java @@ -43,7 +43,7 @@ public class ByteArrayRandomAccessData implements RandomAccessData { } @Override - public InputStream getInputStream() { + public InputStream getInputStream(ResourceAccess access) { return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length); } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java index 2ab0eea7fd..7ae0e74750 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -16,6 +16,7 @@ package org.springframework.boot.loader.data; +import java.io.IOException; import java.io.InputStream; /** @@ -29,9 +30,11 @@ public interface RandomAccessData { /** * Returns an {@link InputStream} that can be used to read the underling data. The * caller is responsible close the underlying stream. + * @param access hint indicating how the underlying data should be accessed * @return a new input stream that can be used to read the underlying data. + * @throws IOException */ - InputStream getInputStream(); + InputStream getInputStream(ResourceAccess access) throws IOException; /** * Returns a new {@link RandomAccessData} for a specific subsection of this data. @@ -47,4 +50,20 @@ public interface RandomAccessData { */ long getSize(); + /** + * Lock modes for accessing the underlying resource. + */ + public static enum ResourceAccess { + + /** + * Obtain access to the underlying resource once and keep it until the stream is + * closed. + */ + ONCE, + + /** + * Obtain access to the underlying resource on each read, releasing it when done. + */ + PER_READ + } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java index 6fd9e01677..d09d88b290 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -33,7 +33,7 @@ public class RandomAccessDataFile implements RandomAccessData { private static final int DEFAULT_CONCURRENT_READS = 4; - private File file; + private final File file; private final FilePool filePool; @@ -78,7 +78,8 @@ public class RandomAccessDataFile implements RandomAccessData { * @param offset the offset of the section * @param length the length of the section */ - private RandomAccessDataFile(FilePool pool, long offset, long length) { + private RandomAccessDataFile(File file, FilePool pool, long offset, long length) { + this.file = file; this.filePool = pool; this.offset = offset; this.length = length; @@ -93,8 +94,8 @@ public class RandomAccessDataFile implements RandomAccessData { } @Override - public InputStream getInputStream() { - return new DataInputStream(); + public InputStream getInputStream(ResourceAccess access) throws IOException { + return new DataInputStream(access); } @Override @@ -102,7 +103,8 @@ public class RandomAccessDataFile implements RandomAccessData { if (offset < 0 || length < 0 || offset + length > this.length) { throw new IndexOutOfBoundsException(); } - return new RandomAccessDataFile(this.filePool, this.offset + offset, length); + return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset, + length); } @Override @@ -120,7 +122,16 @@ public class RandomAccessDataFile implements RandomAccessData { */ private class DataInputStream extends InputStream { - private long position; + private RandomAccessFile file; + + private int position; + + public DataInputStream(ResourceAccess access) throws IOException { + if (access == ResourceAccess.ONCE) { + this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r"); + this.file.seek(RandomAccessDataFile.this.offset); + } + } @Override public int read() throws IOException { @@ -153,23 +164,29 @@ public class RandomAccessDataFile implements RandomAccessData { if (len == 0) { return 0; } - if (cap(len) <= 0) { + int cappedLen = cap(len); + if (cappedLen <= 0) { return -1; } - RandomAccessFile file = RandomAccessDataFile.this.filePool.acquire(); - try { + RandomAccessFile file = this.file; + if (file == null) { + file = RandomAccessDataFile.this.filePool.acquire(); file.seek(RandomAccessDataFile.this.offset + this.position); + } + try { if (b == null) { int rtn = file.read(); moveOn(rtn == -1 ? 0 : 1); return rtn; } else { - return (int) moveOn(file.read(b, off, (int) cap(len))); + return (int) moveOn(file.read(b, off, cappedLen)); } } finally { - RandomAccessDataFile.this.filePool.release(file); + if (this.file == null) { + RandomAccessDataFile.this.filePool.release(file); + } } } @@ -178,14 +195,21 @@ public class RandomAccessDataFile implements RandomAccessData { return (n <= 0 ? 0 : moveOn(cap(n))); } + @Override + public void close() throws IOException { + if (this.file != null) { + this.file.close(); + } + } + /** * Cap the specified value such that it cannot exceed the number of bytes * remaining. * @param n the value to cap * @return the capped value */ - private long cap(long n) { - return Math.min(RandomAccessDataFile.this.length - this.position, n); + private int cap(long n) { + return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); } /** @@ -193,7 +217,7 @@ public class RandomAccessDataFile implements RandomAccessData { * @param amount the amount to move * @return the amount moved */ - private long moveOn(long amount) { + private long moveOn(int amount) { this.position += amount; return amount; } @@ -237,16 +261,16 @@ public class RandomAccessDataFile implements RandomAccessData { public void close() throws IOException { try { - this.available.acquire(size); + this.available.acquire(this.size); try { - RandomAccessFile file = files.poll(); + RandomAccessFile file = this.files.poll(); while (file != null) { file.close(); - file = files.poll(); + file = this.files.poll(); } } finally { - this.available.release(size); + this.available.release(this.size); } } catch (InterruptedException ex) { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java new file mode 100644 index 0000000000..c9d348f150 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess; + +/** + * Utilities for dealing with bytes from ZIP files. + * + * @author Phillip Webb + */ +class Bytes { + + private static final byte[] EMPTY_BYTES = new byte[] {}; + + public static byte[] get(RandomAccessData data) throws IOException { + InputStream inputStream = data.getInputStream(ResourceAccess.ONCE); + try { + return get(inputStream, data.getSize()); + } + finally { + inputStream.close(); + } + } + + public static byte[] get(InputStream inputStream, long length) throws IOException { + if (length == 0) { + return EMPTY_BYTES; + } + byte[] bytes = new byte[(int) length]; + if (!fill(inputStream, bytes)) { + throw new IOException("Unable to read bytes"); + } + return bytes; + } + + public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException { + return fill(inputStream, bytes, 0, bytes.length); + } + + private static boolean fill(InputStream inputStream, byte[] bytes, int offset, + int length) throws IOException { + while (length > 0) { + int read = inputStream.read(bytes, offset, length); + if (read == -1) { + return false; + } + offset += read; + length = -read; + } + return true; + } + + public static long littleEndianValue(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = length - 1; i >= 0; i--) { + value = ((value << 8) | (bytes[offset + i] & 0xFF)); + } + return value; + } +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java new file mode 100644 index 0000000000..b3c40eb599 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.IOException; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * A ZIP File "End of central directory record" (EOCD). + * + * @author Phillip Webb + * @see Zip File Format + */ +class CentralDirectoryEndRecord { + + private static final int MINIMUM_SIZE = 22; + + private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; + + private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; + + private static final int SIGNATURE = 0x06054b50; + + private static final int COMMENT_LENGTH_OFFSET = 20; + + private static final int READ_BLOCK_SIZE = 256; + + private byte[] block; + + private int offset; + + private int size; + + /** + * Create a new {@link CentralDirectoryEndRecord} instance from the specified + * {@link RandomAccessData}, searching backwards from the end until a valid block is + * located. + * @param data the source data + * @throws IOException + */ + public CentralDirectoryEndRecord(RandomAccessData data) throws IOException { + this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE); + this.size = MINIMUM_SIZE; + this.offset = this.block.length - this.size; + while (!isValid()) { + this.size++; + if (this.size > this.block.length) { + if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) { + throw new IOException("Unable to find ZIP central directory " + + "records after reading " + this.size + " bytes"); + } + this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE); + } + this.offset = this.block.length - this.size; + } + } + + private byte[] createBlockFromEndOfData(RandomAccessData data, int size) + throws IOException { + int length = (int) Math.min(data.getSize(), size); + return Bytes.get(data.getSubsection(data.getSize() - length, length)); + } + + private boolean isValid() { + if (this.block.length < MINIMUM_SIZE + || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) { + return false; + } + // Total size must be the structure size + comment + long commentLength = Bytes.littleEndianValue(this.block, this.offset + + COMMENT_LENGTH_OFFSET, 2); + return this.size == MINIMUM_SIZE + commentLength; + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in this + * record. + * @param data the source data + * @return the central directory data + */ + public RandomAccessData getCentralDirectory(RandomAccessData data) { + long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + return data.getSubsection(offset, length); + } + + /** + * Return the number of ZIP entries in the file. + */ + public int getNumberOfRecords() { + return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2); + } +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java new file mode 100644 index 0000000000..f8145c412a --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.IOException; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s. + * + * @author Phillip Webb + */ +public class JarEntry extends java.util.jar.JarEntry { + + private final JarEntryData source; + + private Certificate[] certificates; + + private CodeSigner[] codeSigners; + + public JarEntry(JarEntryData source) { + super(source.getName().toString()); + this.source = source; + } + + /** + * Return the source {@link JarEntryData} that was used to create this entry. + */ + public JarEntryData getSource() { + return this.source; + } + + @Override + public Attributes getAttributes() throws IOException { + Manifest manifest = this.source.getSource().getManifest(); + return (manifest == null ? null : manifest.getAttributes(getName())); + } + + @Override + public Certificate[] getCertificates() { + if (this.source.getSource().isSigned() && this.certificates == null) { + this.source.getSource().setupEntryCertificates(); + } + return this.certificates; + } + + @Override + public CodeSigner[] getCodeSigners() { + if (this.source.getSource().isSigned() && this.codeSigners == null) { + this.source.getSource().setupEntryCertificates(); + } + return this.codeSigners; + } + + void setupCertificates(java.util.jar.JarEntry entry) { + this.certificates = entry.getCertificates(); + this.codeSigners = entry.getCodeSigners(); + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java new file mode 100644 index 0000000000..7c086cac3b --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java @@ -0,0 +1,168 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.AsciiBytes; +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess; + +/** + * Holds the underlying data of a {@link JarEntry}, allowing creation to be deferred until + * the entry is actually needed. + * + * @author Phillip Webb + */ +public final class JarEntryData { + + private static final long LOCAL_FILE_HEADER_SIZE = 30; + + private static final AsciiBytes SLASH = new AsciiBytes("/"); + + private final JarFile source; + + private byte[] header; + + private AsciiBytes name; + + private final byte[] extra; + + private final AsciiBytes comment; + + private long dataOffset; + + private RandomAccessData data; + + private SoftReference entry; + + public JarEntryData(JarFile source, byte[] header, InputStream inputStream) + throws IOException { + + this.source = source; + this.header = header; + long nameLength = Bytes.littleEndianValue(header, 28, 2); + long extraLength = Bytes.littleEndianValue(header, 30, 2); + long commentLength = Bytes.littleEndianValue(header, 32, 2); + + this.name = new AsciiBytes(Bytes.get(inputStream, nameLength)); + this.extra = Bytes.get(inputStream, extraLength); + this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength)); + + this.dataOffset = Bytes.littleEndianValue(header, 42, 4); + this.dataOffset += LOCAL_FILE_HEADER_SIZE; + this.dataOffset += this.name.length(); + this.dataOffset += this.extra.length; + } + + void setName(AsciiBytes name) { + this.name = name; + } + + JarFile getSource() { + return this.source; + } + + InputStream getInputStream() throws IOException { + InputStream inputStream = getData().getInputStream(ResourceAccess.PER_READ); + if (getMethod() == ZipEntry.DEFLATED) { + inputStream = new ZipInflaterInputStream(inputStream, getSize()); + } + return inputStream; + } + + RandomAccessData getData() { + if (this.data == null) { + this.data = this.source.getData().getSubsection(this.dataOffset, + getCompressedSize()); + } + return this.data; + } + + JarEntry asJarEntry() { + JarEntry entry = (this.entry == null ? null : this.entry.get()); + if (entry == null) { + entry = new JarEntry(this); + entry.setCompressedSize(getCompressedSize()); + entry.setMethod(getMethod()); + entry.setCrc(getCrc()); + entry.setSize(getSize()); + entry.setExtra(getExtra()); + entry.setComment(getComment().toString()); + entry.setSize(getSize()); + entry.setTime(getTime()); + this.entry = new SoftReference(entry); + } + return entry; + } + + public AsciiBytes getName() { + return this.name; + } + + public boolean isDirectory() { + return this.name.endsWith(SLASH); + } + + public int getMethod() { + return (int) Bytes.littleEndianValue(this.header, 10, 2); + } + + public long getTime() { + return Bytes.littleEndianValue(this.header, 12, 4); + } + + public long getCrc() { + return Bytes.littleEndianValue(this.header, 16, 4); + } + + public int getCompressedSize() { + return (int) Bytes.littleEndianValue(this.header, 20, 4); + } + + public int getSize() { + return (int) Bytes.littleEndianValue(this.header, 24, 4); + } + + public byte[] getExtra() { + return this.extra; + } + + public AsciiBytes getComment() { + return this.comment; + } + + /** + * Create a new {@link JarEntryData} instance from the specified input stream. + * @param source the source {@link JarFile} + * @param inputStream the input stream to load data from + * @return a {@link JarEntryData} or {@code null} + * @throws IOException + */ + static JarEntryData fromInputStream(JarFile source, InputStream inputStream) + throws IOException { + byte[] header = new byte[46]; + if (!Bytes.fill(inputStream, header)) { + return null; + } + return new JarEntryData(source, header, inputStream); + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java index 43c5602b9e..53284716dd 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -16,7 +16,7 @@ package org.springframework.boot.loader.jar; -import java.util.jar.JarEntry; +import org.springframework.boot.loader.AsciiBytes; /** * Interface that can be used to filter and optionally rename jar entries. @@ -27,12 +27,12 @@ public interface JarEntryFilter { /** * Apply the jar entry filter. - * @param entryName the current entry name. This may be different that the original - * entry name if a previous filter has been applied - * @param entry the entry to filter + * @param name the current entry name. This may be different that the original entry + * name if a previous filter has been applied + * @param entryData the entry data to filter * @return the new name of the entry or {@code null} if the entry should not be * included. */ - String apply(String entryName, JarEntry entry); + AsciiBytes apply(AsciiBytes name, JarEntryData entryData); } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java new file mode 100644 index 0000000000..ba7a08da8c --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -0,0 +1,387 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.AsciiBytes; +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess; +import org.springframework.boot.loader.data.RandomAccessDataFile; + +/** + * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but + * offers the following additional functionality. + *
    + *
  • Jar entries can be {@link JarEntryFilter filtered} during construction and new + * filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from + * existing files.
  • + *
  • A nested {@link JarFile} can be + * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory + * entry.
  • + *
  • A nested {@link JarFile} can be + * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files + * (as long as their entry is not compressed).
  • + *
  • Entry data can be accessed as {@link RandomAccessData}.
  • + *
+ * + * @author Phillip Webb + */ +public class JarFile extends java.util.jar.JarFile implements Iterable { + + private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); + + private static final AsciiBytes MANIFEST_MF = new AsciiBytes("META-INF/MANIFEST.MF"); + + private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); + + private final RandomAccessDataFile rootFile; + + private RandomAccessData data; + + private final String name; + + private final long size; + + private boolean signed; + + private List entries; + + private SoftReference> entriesByName; + + private JarEntryData manifestEntry; + + private SoftReference 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(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); + } + return manifest; + } + + @Override + public Enumeration entries() { + final Iterator iterator = iterator(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public java.util.jar.JarEntry nextElement() { + return iterator.next().asJarEntry(); + } + }; + } + + @Override + public Iterator 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 entriesByName = (this.entriesByName == null ? null + : this.entriesByName.get()); + if (entriesByName == null) { + entriesByName = new HashMap(); + for (JarEntryData entry : this.entries) { + entriesByName.put(entry.getName(), entry); + } + this.entriesByName = new SoftReference>( + 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); + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java new file mode 100644 index 0000000000..b97edfefc8 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -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(); + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarEntry.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLStreamHandler.java similarity index 51% rename from spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarEntry.java rename to spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLStreamHandler.java index 6dfdfc708b..58c7e7ea05 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarEntry.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLStreamHandler.java @@ -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); } } diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStream.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStream.java deleted file mode 100644 index 93ee5d22c7..0000000000 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStream.java +++ /dev/null @@ -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; - } - } - -} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessJarFile.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessJarFile.java deleted file mode 100644 index 5d3eab1136..0000000000 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/RandomAccessJarFile.java +++ /dev/null @@ -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. - *
    - *
  • Jar entries can be {@link JarEntryFilter filtered} during construction and new - * filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from - * existing files.
  • - *
  • A nested {@link JarFile} can be - * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory - * entry.
  • - *
  • A nested {@link JarFile} can be - * {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files - * (as long as their entry is not compressed).
  • - *
  • Entry data can be accessed as {@link RandomAccessData}.
  • - *
- * - * @author Phillip Webb - */ -public class 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 entries = new LinkedHashMap(); - - 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 originalEntries = this.entries; - this.entries = new LinkedHashMap(); - - 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 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); - } - } - - } - -} diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java new file mode 100644 index 0000000000..43b15b0530 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -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); + } + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java new file mode 100644 index 0000000000..1ac23ef8c8 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AsciiBytesTests.java @@ -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))); + } +} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java index 0e89f9e3c7..5a0daa0686 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -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 getEntriesMap(Archive archive) { Map entries = new HashMap(); for (Archive.Entry entry : archive.getEntries()) { - entries.put(entry.getName(), entry); + entries.put(entry.getName().toString(), entry); } return entries; } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index c20ec468fb..d8f93e7168 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -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 getEntriesMap(Archive archive) { Map entries = new HashMap(); for (Archive.Entry entry : archive.getEntries()) { - entries.put(entry.getName(), entry); + entries.put(entry.getName().toString(), entry); } return entries; } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/ByteArrayRandomAccessDataTest.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/ByteArrayRandomAccessDataTest.java index 6bf2b8b954..8ef8064aa7 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/ByteArrayRandomAccessDataTest.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/ByteArrayRandomAccessDataTest.java @@ -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)); } } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java index 147cdd7e42..ea49701e4c 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java @@ -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); diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStreamTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStreamTests.java deleted file mode 100644 index ac73a0fd3c..0000000000 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessDataJarInputStreamTests.java +++ /dev/null @@ -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(); - } - } - -} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessJarFileTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessJarFileTests.java index 660011b42a..0c8233206b 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessJarFileTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/RandomAccessJarFileTests.java @@ -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 entries = this.jarFile.entries(); + Enumeration 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 entries = nestedJarFile.entries(); + Enumeration 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 entries = nestedJarFile.entries(); + Enumeration 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 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 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 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()); + } + } + } }