Commit dbfd785e authored by Phillip Webb's avatar Phillip Webb

Merge branch 'gh-1119'

Improve fat JAR performance.

See gh-1119
parents 378d38e2 20fb55ea
......@@ -44,4 +44,5 @@ public class JarLauncher extends ExecutableArchiveLauncher {
public static void main(String[] args) {
new JarLauncher().launch(args);
}
}
......@@ -32,4 +32,5 @@ public interface JavaAgentDetector {
* @param url The url to examine
*/
public boolean isJavaAgentJar(URL url);
}
......@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import org.springframework.boot.loader.jar.Handler;
import org.springframework.boot.loader.jar.JarFile;
/**
......@@ -93,7 +94,6 @@ public class LaunchedURLClassLoader extends URLClassLoader {
@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (this.rootClassLoader == null) {
return findResources(name);
}
......@@ -116,6 +116,7 @@ public class LaunchedURLClassLoader extends URLClassLoader {
}
return localResources.nextElement();
}
};
}
......@@ -128,7 +129,13 @@ public class LaunchedURLClassLoader extends URLClassLoader {
synchronized (this) {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
loadedClass = doLoadClass(name);
Handler.setUseFastConnectionExceptions(true);
try {
loadedClass = doLoadClass(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
if (resolve) {
resolveClass(loadedClass);
......@@ -214,4 +221,5 @@ public class LaunchedURLClassLoader extends URLClassLoader {
// Ignore
}
}
}
......@@ -79,4 +79,5 @@ public class WarLauncher extends ExecutableArchiveLauncher {
public static void main(String[] args) {
new WarLauncher().launch(args);
}
}
......@@ -65,5 +65,7 @@ public interface RandomAccessData {
* Obtain access to the underlying resource on each read, releasing it when done.
*/
PER_READ
}
}
......@@ -221,6 +221,7 @@ public class RandomAccessDataFile implements RandomAccessData {
this.position += amount;
return amount;
}
}
/**
......@@ -277,5 +278,7 @@ public class RandomAccessDataFile implements RandomAccessData {
throw new IOException(ex);
}
}
}
}
......@@ -119,4 +119,5 @@ class CentralDirectoryEndRecord {
public int getNumberOfRecords() {
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2);
}
}
......@@ -18,11 +18,14 @@ package org.springframework.boot.loader.jar;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -39,7 +42,7 @@ public class Handler extends URLStreamHandler {
private static final String FILE_PROTOCOL = "file:";
private static final String SEPARATOR = JarURLConnection.SEPARATOR;
private static final String SEPARATOR = "!/";
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };
......@@ -55,6 +58,11 @@ public class Handler extends URLStreamHandler {
OPEN_CONNECTION_METHOD = method;
}
private static SoftReference<Map<File, JarFile>> rootFileCache;
static {
rootFileCache = new SoftReference<Map<File, JarFile>>(null);
}
private final Logger logger = Logger.getLogger(getClass().getName());
private final JarFile jarFile;
......@@ -153,7 +161,14 @@ public class Handler extends URLStreamHandler {
throw new IllegalStateException("Not a file URL");
}
String path = name.substring(FILE_PROTOCOL.length());
return new JarFile(new File(path));
File file = new File(path);
Map<File, JarFile> cache = rootFileCache.get();
JarFile jarFile = (cache == null ? null : cache.get(file));
if (jarFile == null) {
jarFile = new JarFile(file);
addToRootFileCache(file, jarFile);
}
return jarFile;
}
catch (Exception ex) {
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
......@@ -168,4 +183,29 @@ public class Handler extends URLStreamHandler {
}
return jarFile.getNestedJarFile(jarEntry);
}
/**
* Add the given {@link JarFile} to the root file cache.
* @param sourceFile the source file to add
* @param jarFile the jar file.
*/
static void addToRootFileCache(File sourceFile, JarFile jarFile) {
Map<File, JarFile> cache = rootFileCache.get();
if (cache == null) {
cache = new ConcurrentHashMap<File, JarFile>();
rootFileCache = new SoftReference<Map<File, JarFile>>(cache);
}
cache.put(sourceFile, jarFile);
}
/**
* Set if a generic static exception can be thrown when a URL cannot be connected.
* This optimization is used during class loading to save creating lots of exceptions
* which are then swallowed.
* @param useFastConnectionExceptions if fast connection exceptions can be used.
*/
public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) {
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
}
}
......@@ -53,22 +53,30 @@ public final class JarEntryData {
private SoftReference<JarEntry> entry;
JarFile nestedJar;
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.localHeaderOffset = Bytes.littleEndianValue(header, 42, 4);
}
private JarEntryData(JarEntryData master, JarFile source, AsciiBytes name) {
this.header = master.header;
this.extra = master.extra;
this.comment = master.comment;
this.localHeaderOffset = master.localHeaderOffset;
this.source = source;
this.name = name;
}
void setName(AsciiBytes name) {
this.name = name;
}
......@@ -154,6 +162,10 @@ public final class JarEntryData {
return this.comment;
}
JarEntryData createFilteredCopy(JarFile jarFile, AsciiBytes name) {
return new JarEntryData(this, jarFile, name);
}
/**
* Create a new {@link JarEntryData} instance from the specified input stream.
* @param source the source {@link JarFile}
......
......@@ -43,15 +43,12 @@ import org.springframework.boot.loader.util.AsciiBytes;
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
* existing files.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
* entry.</li>
* <li>A nested {@link JarFile} can be
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files
* (as long as their entry is not compressed).</li>
* <li>New filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created}
* from existing files.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
* </ul>
*
......@@ -69,20 +66,20 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
private final RandomAccessDataFile rootFile;
private static final AsciiBytes SLASH = new AsciiBytes("/");
private final RandomAccessData data;
private final RandomAccessDataFile rootFile;
private final String name;
private final long size;
private boolean signed;
private final RandomAccessData data;
private List<JarEntryData> entries;
private final List<JarEntryData> entries;
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
private boolean signed;
private JarEntryData manifestEntry;
private SoftReference<Manifest> manifest;
......@@ -90,21 +87,19 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
/**
* 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);
public JarFile(File file) throws IOException {
this(new RandomAccessDataFile(file));
}
/**
* 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().getAbsolutePath(), file, filters);
JarFile(RandomAccessDataFile file) throws IOException {
this(file, file.getFile().getAbsolutePath(), file);
}
/**
......@@ -113,18 +108,25 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
* @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 {
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data)
throws IOException {
super(rootFile.getFile());
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
this.rootFile = rootFile;
this.name = name;
this.data = getArchiveData(endRecord, data);
this.entries = loadJarEntries(endRecord);
}
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data,
List<JarEntryData> entries, JarEntryFilter... filters) throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.name = name;
this.size = data.getSize();
loadJarEntries(endRecord, filters);
this.data = data;
this.entries = filterEntries(entries, filters);
}
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
......@@ -136,36 +138,48 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
return data.getSubsection(offset, data.getSize() - offset);
}
private void loadJarEntries(CentralDirectoryEndRecord endRecord,
JarEntryFilter[] filters) throws IOException {
private List<JarEntryData> loadJarEntries(CentralDirectoryEndRecord endRecord)
throws IOException {
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
int numberOfRecords = endRecord.getNumberOfRecords();
this.entries = new ArrayList<JarEntryData>(numberOfRecords);
List<JarEntryData> entries = new ArrayList<JarEntryData>(numberOfRecords);
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
try {
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
while (entry != null) {
addJarEntry(entry, filters);
entries.add(entry);
processEntry(entry);
entry = JarEntryData.fromInputStream(this, inputStream);
}
}
finally {
inputStream.close();
}
return entries;
}
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 List<JarEntryData> filterEntries(List<JarEntryData> entries,
JarEntryFilter[] filters) {
List<JarEntryData> filteredEntries = new ArrayList<JarEntryData>(entries.size());
for (JarEntryData entry : entries) {
AsciiBytes name = entry.getName();
for (JarEntryFilter filter : filters) {
name = (filter == null || name == null ? name : filter.apply(name, entry));
}
if (name != null) {
JarEntryData filteredCopy = entry.createFilteredCopy(this, name);
filteredEntries.add(filteredCopy);
processEntry(filteredCopy);
}
}
return filteredEntries;
}
private void processEntry(JarEntryData entry) {
AsciiBytes name = entry.getName();
if (name.startsWith(META_INF)) {
processMetaInfEntry(name, entry);
}
}
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
......@@ -238,6 +252,13 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
}
public JarEntryData getJarEntryData(String name) {
if (name == null) {
return null;
}
return getJarEntryData(new AsciiBytes(name));
}
public JarEntryData getJarEntryData(AsciiBytes name) {
if (name == null) {
return null;
}
......@@ -252,9 +273,9 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
entriesByName);
}
JarEntryData entryData = entriesByName.get(new AsciiBytes(name));
if (entryData == null && !name.endsWith("/")) {
entryData = entriesByName.get(new AsciiBytes(name + "/"));
JarEntryData entryData = entriesByName.get(name);
if (entryData == null && !name.endsWith(SLASH)) {
entryData = entriesByName.get(name.append(SLASH));
}
return entryData;
}
......@@ -297,29 +318,26 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
/**
* 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 {
public synchronized JarFile getNestedJarFile(final ZipEntry ze) 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 {
public synchronized JarFile getNestedJarFile(JarEntryData sourceEntry)
throws IOException {
try {
if (sourceEntry.isDirectory()) {
return getNestedJarFileFromDirectoryEntry(sourceEntry, filters);
if (sourceEntry.nestedJar == null) {
sourceEntry.nestedJar = createJarFileFromEntry(sourceEntry);
}
return getNestedJarFileFromFileEntry(sourceEntry, filters);
return sourceEntry.nestedJar;
}
catch (IOException ex) {
throw new IOException("Unable to open nested jar file '"
......@@ -327,12 +345,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
}
}
private JarFile getNestedJarFileFromDirectoryEntry(JarEntryData sourceEntry,
JarEntryFilter... filters) throws IOException {
private JarFile createJarFileFromEntry(JarEntryData sourceEntry) throws IOException {
if (sourceEntry.isDirectory()) {
return createJarFileFromDirectoryEntry(sourceEntry);
}
return createJarFileFromFileEntry(sourceEntry);
}
private JarFile createJarFileFromDirectoryEntry(JarEntryData sourceEntry)
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() {
JarEntryFilter filter = new JarEntryFilter() {
@Override
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
......@@ -343,17 +366,17 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
};
return new JarFile(this.rootFile, getName() + "!/"
+ sourceEntry.getName().substring(0, sourceName.length() - 1), this.data,
filtersToUse);
this.entries, filter);
}
private JarFile getNestedJarFileFromFileEntry(JarEntryData sourceEntry,
JarEntryFilter... filters) throws IOException {
private JarFile createJarFileFromFileEntry(JarEntryData sourceEntry)
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);
sourceEntry.getData());
}
/**
......@@ -364,7 +387,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
*/
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
throws IOException {
return new JarFile(this.rootFile, getName(), this.data, filters);
return new JarFile(this.rootFile, getName(), this.data, this.entries, filters);
}
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
......@@ -377,7 +400,7 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
@Override
public int size() {
return (int) this.size;
return (int) this.data.getSize();
}
@Override
......@@ -455,4 +478,5 @@ public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryD
*/
SYSTEM_INDEPENDENT
}
}
......@@ -22,6 +22,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.jar.Manifest;
import org.springframework.boot.loader.util.AsciiBytes;
......@@ -33,51 +35,73 @@ import org.springframework.boot.loader.util.AsciiBytes;
*/
class JarURLConnection extends java.net.JarURLConnection {
static final String PROTOCOL = "jar";
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException();
static final String SEPARATOR = "!/";
private static final String SEPARATOR = "!/";
private static final String PREFIX = PROTOCOL + ":" + "file:";
private static final URL EMPTY_JAR_URL;
private final JarFile jarFile;
static {
try {
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
// Instantiated and cached.
return null;
}
});
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private JarEntryData jarEntryData;
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
private String jarEntryName;
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private String contentType;
private final String jarFileUrlSpec;
private final JarFile jarFile;
private JarEntryData jarEntryData;
private URL jarFileUrl;
private JarEntryName jarEntryName;
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
super(new URL(buildRootUrl(jarFile)));
// What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url;
this.jarFile = jarFile;
String spec = url.getFile();
int separator = spec.lastIndexOf(SEPARATOR);
if (separator == -1) {
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
+ spec);
}
if (separator + 2 != spec.length()) {
this.jarEntryName = decode(spec.substring(separator + 2));
}
this.jarFileUrlSpec = spec.substring(0, separator);
this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
}
String container = spec.substring(0, separator);
if (container.indexOf(SEPARATOR) == -1) {
this.jarFileUrl = new URL(container);
}
else {
this.jarFileUrl = new URL("jar:" + container);
private JarEntryName getJarEntryName(String spec) {
if (spec.length() == 0) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec);
}
@Override
public void connect() throws IOException {
if (this.jarEntryName != null) {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName);
if (!this.jarEntryName.isEmpty()) {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
.asAsciiBytes());
if (this.jarEntryData == null) {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName());
}
......@@ -103,9 +127,24 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public URL getJarFileURL() {
if (this.jarFileUrl == null) {
this.jarFileUrl = buildJarFileUrl();
}
return this.jarFileUrl;
}
private URL buildJarFileUrl() {
try {
if (this.jarFileUrlSpec.indexOf(SEPARATOR) == -1) {
return new URL(this.jarFileUrlSpec);
}
return new URL("jar:" + this.jarFileUrlSpec);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public JarEntry getJarEntry() throws IOException {
connect();
......@@ -114,13 +153,13 @@ class JarURLConnection extends java.net.JarURLConnection {
@Override
public String getEntryName() {
return this.jarEntryName;
return this.jarEntryName.toString();
}
@Override
public InputStream getInputStream() throws IOException {
connect();
if (this.jarEntryName == null) {
if (this.jarEntryName.isEmpty()) {
throw new IOException("no entry name specified");
}
return this.jarEntryData.getInputStream();
......@@ -130,8 +169,10 @@ class JarURLConnection extends java.net.JarURLConnection {
public int getContentLength() {
try {
connect();
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData
.getSize();
if (this.jarEntryData != null) {
return this.jarEntryData.getSize();
}
return this.jarFile.size();
}
catch (IOException ex) {
return -1;
......@@ -146,58 +187,86 @@ class JarURLConnection extends java.net.JarURLConnection {
@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(PREFIX.length() + path.length()
+ SEPARATOR.length());
builder.append(PREFIX);
builder.append(path);
builder.append(SEPARATOR);
return builder.toString();
}
private static String decode(String source) {
int length = source.length();
if ((length == 0) || (source.indexOf('%') < 0)) {
return source;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
for (int i = 0; i < length; i++) {
int ch = source.charAt(i);
if (ch == '%') {
if ((i + 2) >= length) {
throw new IllegalArgumentException("Invalid encoded sequence \""
+ source.substring(i) + "\"");
return this.jarEntryName.getContentType();
}
static void setUseFastExceptions(boolean useFastExceptions) {
JarURLConnection.useFastExceptions.set(useFastExceptions);
}
/**
* A JarEntryName parsed from a URL String.
*/
private static class JarEntryName {
private final AsciiBytes name;
private String contentType;
public JarEntryName(String spec) {
this.name = decode(spec);
}
private AsciiBytes decode(String source) {
int length = (source == null ? 0 : source.length());
if ((length == 0) || (source.indexOf('%') < 0)) {
return new AsciiBytes(source);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
for (int i = 0; i < length; i++) {
int ch = source.charAt(i);
if (ch == '%') {
if ((i + 2) >= length) {
throw new IllegalArgumentException("Invalid encoded sequence \""
+ source.substring(i) + "\"");
}
ch = decodeEscapeSequence(source, i);
i += 2;
}
ch = decodeEscapeSequence(source, i);
i += 2;
bos.write(ch);
}
bos.write(ch);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray());
}
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new AsciiBytes(bos.toByteArray()).toString();
}
private char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \""
+ source.substring(i) + "\"");
}
return ((char) ((hi << 4) + lo));
}
@Override
public String toString() {
return this.name.toString();
}
public AsciiBytes asAsciiBytes() {
return this.name;
}
public boolean isEmpty() {
return this.name.length() == 0;
}
public String getContentType() {
if (this.contentType == null) {
this.contentType = deduceContentType();
}
return this.contentType;
}
private static char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \""
+ source.substring(i) + "\"");
private String deduceContentType() {
// Guess the content type, don't bother with streams as mark is not supported
String type = (isEmpty() ? "x-java/jar" : null);
type = (type != null ? type : guessContentTypeFromName(toString()));
type = (type != null ? type : "content/unknown");
return type;
}
return ((char) ((hi << 4) + lo));
}
}
......@@ -128,6 +128,13 @@ public final class AsciiBytes {
return append(string.getBytes(UTF_8));
}
public AsciiBytes append(AsciiBytes asciiBytes) {
if (asciiBytes == null || asciiBytes.length() == 0) {
return this;
}
return append(asciiBytes.bytes);
}
public AsciiBytes append(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return this;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment