Improve startup performance for nested JARs
Refactor spring-boot-loader to work directly with low level zip data structures, removing the need to read every byte when the application loads. This change was initially driven by the desire to improve tab-completion time when working with the Spring CLI tool. Local tests show CLI startup time improving from ~0.7 to ~0.22 seconds. Startup times for regular Spring Boot applications are also improved, for example, the tomcat sample application now starts 0.5 seconds faster.pull/123/head
parent
6a6159f106
commit
d2678e08de
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for dealing with bytes from ZIP files.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class Bytes {
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTES = new byte[] {};
|
||||||
|
|
||||||
|
public static byte[] get(RandomAccessData data) throws IOException {
|
||||||
|
InputStream inputStream = data.getInputStream(ResourceAccess.ONCE);
|
||||||
|
try {
|
||||||
|
return get(inputStream, data.getSize());
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] get(InputStream inputStream, long length) throws IOException {
|
||||||
|
if (length == 0) {
|
||||||
|
return EMPTY_BYTES;
|
||||||
|
}
|
||||||
|
byte[] bytes = new byte[(int) length];
|
||||||
|
if (!fill(inputStream, bytes)) {
|
||||||
|
throw new IOException("Unable to read bytes");
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException {
|
||||||
|
return fill(inputStream, bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean fill(InputStream inputStream, byte[] bytes, int offset,
|
||||||
|
int length) throws IOException {
|
||||||
|
while (length > 0) {
|
||||||
|
int read = inputStream.read(bytes, offset, length);
|
||||||
|
if (read == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
offset += read;
|
||||||
|
length = -read;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long littleEndianValue(byte[] bytes, int offset, int length) {
|
||||||
|
long value = 0;
|
||||||
|
for (int i = length - 1; i >= 0; i--) {
|
||||||
|
value = ((value << 8) | (bytes[offset + i] & 0xFF));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ZIP File "End of central directory record" (EOCD).
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @see <a href="http://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
|
||||||
|
*/
|
||||||
|
class CentralDirectoryEndRecord {
|
||||||
|
|
||||||
|
private static final int MINIMUM_SIZE = 22;
|
||||||
|
|
||||||
|
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
|
||||||
|
|
||||||
|
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
|
||||||
|
|
||||||
|
private static final int SIGNATURE = 0x06054b50;
|
||||||
|
|
||||||
|
private static final int COMMENT_LENGTH_OFFSET = 20;
|
||||||
|
|
||||||
|
private static final int READ_BLOCK_SIZE = 256;
|
||||||
|
|
||||||
|
private byte[] block;
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CentralDirectoryEndRecord} instance from the specified
|
||||||
|
* {@link RandomAccessData}, searching backwards from the end until a valid block is
|
||||||
|
* located.
|
||||||
|
* @param data the source data
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public CentralDirectoryEndRecord(RandomAccessData data) throws IOException {
|
||||||
|
this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);
|
||||||
|
this.size = MINIMUM_SIZE;
|
||||||
|
this.offset = this.block.length - this.size;
|
||||||
|
while (!isValid()) {
|
||||||
|
this.size++;
|
||||||
|
if (this.size > this.block.length) {
|
||||||
|
if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {
|
||||||
|
throw new IOException("Unable to find ZIP central directory "
|
||||||
|
+ "records after reading " + this.size + " bytes");
|
||||||
|
}
|
||||||
|
this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);
|
||||||
|
}
|
||||||
|
this.offset = this.block.length - this.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createBlockFromEndOfData(RandomAccessData data, int size)
|
||||||
|
throws IOException {
|
||||||
|
int length = (int) Math.min(data.getSize(), size);
|
||||||
|
return Bytes.get(data.getSubsection(data.getSize() - length, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid() {
|
||||||
|
if (this.block.length < MINIMUM_SIZE
|
||||||
|
|| Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Total size must be the structure size + comment
|
||||||
|
long commentLength = Bytes.littleEndianValue(this.block, this.offset
|
||||||
|
+ COMMENT_LENGTH_OFFSET, 2);
|
||||||
|
return this.size == MINIMUM_SIZE + commentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the bytes of the "Central directory" based on the offset indicated in this
|
||||||
|
* record.
|
||||||
|
* @param data the source data
|
||||||
|
* @return the central directory data
|
||||||
|
*/
|
||||||
|
public RandomAccessData getCentralDirectory(RandomAccessData data) {
|
||||||
|
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
|
||||||
|
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
|
||||||
|
return data.getSubsection(offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of ZIP entries in the file.
|
||||||
|
*/
|
||||||
|
public int getNumberOfRecords() {
|
||||||
|
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.CodeSigner;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class JarEntry extends java.util.jar.JarEntry {
|
||||||
|
|
||||||
|
private final JarEntryData source;
|
||||||
|
|
||||||
|
private Certificate[] certificates;
|
||||||
|
|
||||||
|
private CodeSigner[] codeSigners;
|
||||||
|
|
||||||
|
public JarEntry(JarEntryData source) {
|
||||||
|
super(source.getName().toString());
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the source {@link JarEntryData} that was used to create this entry.
|
||||||
|
*/
|
||||||
|
public JarEntryData getSource() {
|
||||||
|
return this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() throws IOException {
|
||||||
|
Manifest manifest = this.source.getSource().getManifest();
|
||||||
|
return (manifest == null ? null : manifest.getAttributes(getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate[] getCertificates() {
|
||||||
|
if (this.source.getSource().isSigned() && this.certificates == null) {
|
||||||
|
this.source.getSource().setupEntryCertificates();
|
||||||
|
}
|
||||||
|
return this.certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CodeSigner[] getCodeSigners() {
|
||||||
|
if (this.source.getSource().isSigned() && this.codeSigners == null) {
|
||||||
|
this.source.getSource().setupEntryCertificates();
|
||||||
|
}
|
||||||
|
return this.codeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupCertificates(java.util.jar.JarEntry entry) {
|
||||||
|
this.certificates = entry.getCertificates();
|
||||||
|
this.codeSigners = entry.getCodeSigners();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.AsciiBytes;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the underlying data of a {@link JarEntry}, allowing creation to be deferred until
|
||||||
|
* the entry is actually needed.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public final class JarEntryData {
|
||||||
|
|
||||||
|
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
||||||
|
|
||||||
|
private static final AsciiBytes SLASH = new AsciiBytes("/");
|
||||||
|
|
||||||
|
private final JarFile source;
|
||||||
|
|
||||||
|
private byte[] header;
|
||||||
|
|
||||||
|
private AsciiBytes name;
|
||||||
|
|
||||||
|
private final byte[] extra;
|
||||||
|
|
||||||
|
private final AsciiBytes comment;
|
||||||
|
|
||||||
|
private long dataOffset;
|
||||||
|
|
||||||
|
private RandomAccessData data;
|
||||||
|
|
||||||
|
private SoftReference<JarEntry> entry;
|
||||||
|
|
||||||
|
public JarEntryData(JarFile source, byte[] header, InputStream inputStream)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
this.source = source;
|
||||||
|
this.header = header;
|
||||||
|
long nameLength = Bytes.littleEndianValue(header, 28, 2);
|
||||||
|
long extraLength = Bytes.littleEndianValue(header, 30, 2);
|
||||||
|
long commentLength = Bytes.littleEndianValue(header, 32, 2);
|
||||||
|
|
||||||
|
this.name = new AsciiBytes(Bytes.get(inputStream, nameLength));
|
||||||
|
this.extra = Bytes.get(inputStream, extraLength);
|
||||||
|
this.comment = new AsciiBytes(Bytes.get(inputStream, commentLength));
|
||||||
|
|
||||||
|
this.dataOffset = Bytes.littleEndianValue(header, 42, 4);
|
||||||
|
this.dataOffset += LOCAL_FILE_HEADER_SIZE;
|
||||||
|
this.dataOffset += this.name.length();
|
||||||
|
this.dataOffset += this.extra.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setName(AsciiBytes name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarFile getSource() {
|
||||||
|
return this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream getInputStream() throws IOException {
|
||||||
|
InputStream inputStream = getData().getInputStream(ResourceAccess.PER_READ);
|
||||||
|
if (getMethod() == ZipEntry.DEFLATED) {
|
||||||
|
inputStream = new ZipInflaterInputStream(inputStream, getSize());
|
||||||
|
}
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccessData getData() {
|
||||||
|
if (this.data == null) {
|
||||||
|
this.data = this.source.getData().getSubsection(this.dataOffset,
|
||||||
|
getCompressedSize());
|
||||||
|
}
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntry asJarEntry() {
|
||||||
|
JarEntry entry = (this.entry == null ? null : this.entry.get());
|
||||||
|
if (entry == null) {
|
||||||
|
entry = new JarEntry(this);
|
||||||
|
entry.setCompressedSize(getCompressedSize());
|
||||||
|
entry.setMethod(getMethod());
|
||||||
|
entry.setCrc(getCrc());
|
||||||
|
entry.setSize(getSize());
|
||||||
|
entry.setExtra(getExtra());
|
||||||
|
entry.setComment(getComment().toString());
|
||||||
|
entry.setSize(getSize());
|
||||||
|
entry.setTime(getTime());
|
||||||
|
this.entry = new SoftReference<JarEntry>(entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsciiBytes getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return this.name.endsWith(SLASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMethod() {
|
||||||
|
return (int) Bytes.littleEndianValue(this.header, 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTime() {
|
||||||
|
return Bytes.littleEndianValue(this.header, 12, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCrc() {
|
||||||
|
return Bytes.littleEndianValue(this.header, 16, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCompressedSize() {
|
||||||
|
return (int) Bytes.littleEndianValue(this.header, 20, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return (int) Bytes.littleEndianValue(this.header, 24, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getExtra() {
|
||||||
|
return this.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsciiBytes getComment() {
|
||||||
|
return this.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JarEntryData} instance from the specified input stream.
|
||||||
|
* @param source the source {@link JarFile}
|
||||||
|
* @param inputStream the input stream to load data from
|
||||||
|
* @return a {@link JarEntryData} or {@code null}
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
static JarEntryData fromInputStream(JarFile source, InputStream inputStream)
|
||||||
|
throws IOException {
|
||||||
|
byte[] header = new byte[46];
|
||||||
|
if (!Bytes.fill(inputStream, header)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JarEntryData(source, header, inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.jar.JarInputStream;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import org.springframework.boot.loader.AsciiBytes;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||||
|
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
|
||||||
|
* offers the following additional functionality.
|
||||||
|
* <ul>
|
||||||
|
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
|
||||||
|
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
|
||||||
|
* existing files.</li>
|
||||||
|
* <li>A nested {@link JarFile} can be
|
||||||
|
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
|
||||||
|
* entry.</li>
|
||||||
|
* <li>A nested {@link JarFile} can be
|
||||||
|
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files
|
||||||
|
* (as long as their entry is not compressed).</li>
|
||||||
|
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class JarFile extends java.util.jar.JarFile implements Iterable<JarEntryData> {
|
||||||
|
|
||||||
|
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
|
||||||
|
|
||||||
|
private static final AsciiBytes MANIFEST_MF = new AsciiBytes("META-INF/MANIFEST.MF");
|
||||||
|
|
||||||
|
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
|
||||||
|
|
||||||
|
private final RandomAccessDataFile rootFile;
|
||||||
|
|
||||||
|
private RandomAccessData data;
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final long size;
|
||||||
|
|
||||||
|
private boolean signed;
|
||||||
|
|
||||||
|
private List<JarEntryData> entries;
|
||||||
|
|
||||||
|
private SoftReference<Map<AsciiBytes, JarEntryData>> entriesByName;
|
||||||
|
|
||||||
|
private JarEntryData manifestEntry;
|
||||||
|
|
||||||
|
private SoftReference<Manifest> manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JarFile} backed by the specified file.
|
||||||
|
* @param file the root jar file
|
||||||
|
* @param filters an optional set of jar entry filters
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public JarFile(File file, JarEntryFilter... filters) throws IOException {
|
||||||
|
this(new RandomAccessDataFile(file), filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JarFile} backed by the specified file.
|
||||||
|
* @param file the root jar file
|
||||||
|
* @param filters an optional set of jar entry filters
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
JarFile(RandomAccessDataFile file, JarEntryFilter... filters) throws IOException {
|
||||||
|
this(file, file.getFile().getPath(), file, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor used to create a new {@link JarFile} either directly or from a
|
||||||
|
* nested entry.
|
||||||
|
* @param rootFile the root jar file
|
||||||
|
* @param name the name of this file
|
||||||
|
* @param data the underlying data
|
||||||
|
* @param filters an optional set of jar entry filters
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private JarFile(RandomAccessDataFile rootFile, String name, RandomAccessData data,
|
||||||
|
JarEntryFilter... filters) throws IOException {
|
||||||
|
super(rootFile.getFile());
|
||||||
|
this.rootFile = rootFile;
|
||||||
|
this.name = name;
|
||||||
|
this.data = data;
|
||||||
|
this.size = data.getSize();
|
||||||
|
loadJarEntries(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadJarEntries(JarEntryFilter[] filters) throws IOException {
|
||||||
|
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(this.data);
|
||||||
|
RandomAccessData centralDirectory = endRecord.getCentralDirectory(this.data);
|
||||||
|
int numberOfRecords = endRecord.getNumberOfRecords();
|
||||||
|
this.entries = new ArrayList<JarEntryData>(numberOfRecords);
|
||||||
|
InputStream inputStream = centralDirectory.getInputStream(ResourceAccess.ONCE);
|
||||||
|
try {
|
||||||
|
JarEntryData entry = JarEntryData.fromInputStream(this, inputStream);
|
||||||
|
while (entry != null) {
|
||||||
|
addJarEntry(entry, filters);
|
||||||
|
entry = JarEntryData.fromInputStream(this, inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addJarEntry(JarEntryData entry, JarEntryFilter[] filters) {
|
||||||
|
AsciiBytes name = entry.getName();
|
||||||
|
for (JarEntryFilter filter : filters) {
|
||||||
|
name = (filter == null || name == null ? name : filter.apply(name, entry));
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
entry.setName(name);
|
||||||
|
this.entries.add(entry);
|
||||||
|
if (name.startsWith(META_INF)) {
|
||||||
|
processMetaInfEntry(name, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processMetaInfEntry(AsciiBytes name, JarEntryData entry) {
|
||||||
|
if (name.equals(MANIFEST_MF)) {
|
||||||
|
this.manifestEntry = entry;
|
||||||
|
}
|
||||||
|
if (name.endsWith(SIGNATURE_FILE_EXTENSION)) {
|
||||||
|
this.signed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final RandomAccessDataFile getRootJarFile() {
|
||||||
|
return this.rootFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomAccessData getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Manifest getManifest() throws IOException {
|
||||||
|
if (this.manifestEntry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
|
||||||
|
if (manifest == null) {
|
||||||
|
InputStream inputStream = this.manifestEntry.getInputStream();
|
||||||
|
try {
|
||||||
|
manifest = new Manifest(inputStream);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
this.manifest = new SoftReference<Manifest>(manifest);
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<java.util.jar.JarEntry> entries() {
|
||||||
|
final Iterator<JarEntryData> iterator = iterator();
|
||||||
|
return new Enumeration<java.util.jar.JarEntry>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasMoreElements() {
|
||||||
|
return iterator.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.jar.JarEntry nextElement() {
|
||||||
|
return iterator.next().asJarEntry();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<JarEntryData> iterator() {
|
||||||
|
return this.entries.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry getJarEntry(String name) {
|
||||||
|
return (JarEntry) getEntry(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZipEntry getEntry(String name) {
|
||||||
|
JarEntryData jarEntryData = getJarEntryData(name);
|
||||||
|
return (jarEntryData == null ? null : jarEntryData.asJarEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
public JarEntryData getJarEntryData(String name) {
|
||||||
|
if (name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<AsciiBytes, JarEntryData> entriesByName = (this.entriesByName == null ? null
|
||||||
|
: this.entriesByName.get());
|
||||||
|
if (entriesByName == null) {
|
||||||
|
entriesByName = new HashMap<AsciiBytes, JarEntryData>();
|
||||||
|
for (JarEntryData entry : this.entries) {
|
||||||
|
entriesByName.put(entry.getName(), entry);
|
||||||
|
}
|
||||||
|
this.entriesByName = new SoftReference<Map<AsciiBytes, JarEntryData>>(
|
||||||
|
entriesByName);
|
||||||
|
}
|
||||||
|
|
||||||
|
JarEntryData entryData = entriesByName.get(new AsciiBytes(name));
|
||||||
|
if (entryData == null && !name.endsWith("/")) {
|
||||||
|
entryData = entriesByName.get(new AsciiBytes(name + "/"));
|
||||||
|
}
|
||||||
|
return entryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSigned() {
|
||||||
|
return this.signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupEntryCertificates() {
|
||||||
|
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
|
||||||
|
// happening that often.
|
||||||
|
try {
|
||||||
|
JarInputStream inputStream = new JarInputStream(getData().getInputStream(
|
||||||
|
ResourceAccess.ONCE));
|
||||||
|
try {
|
||||||
|
java.util.jar.JarEntry entry = inputStream.getNextJarEntry();
|
||||||
|
while (entry != null) {
|
||||||
|
inputStream.closeEntry();
|
||||||
|
JarEntry jarEntry = getJarEntry(entry.getName());
|
||||||
|
if (jarEntry != null) {
|
||||||
|
jarEntry.setupCertificates(entry);
|
||||||
|
}
|
||||||
|
entry = inputStream.getNextJarEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||||
|
return getContainedEntry(ze).getSource().getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a nested {@link JarFile} loaded from the specified entry.
|
||||||
|
* @param ze the zip entry
|
||||||
|
* @param filters an optional set of jar entry filters to be applied
|
||||||
|
* @return a {@link JarFile} for the entry
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public synchronized JarFile getNestedJarFile(final ZipEntry ze,
|
||||||
|
JarEntryFilter... filters) throws IOException {
|
||||||
|
return getNestedJarFile(getContainedEntry(ze).getSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a nested {@link JarFile} loaded from the specified entry.
|
||||||
|
* @param sourceEntry the zip entry
|
||||||
|
* @param filters an optional set of jar entry filters to be applied
|
||||||
|
* @return a {@link JarFile} for the entry
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public synchronized JarFile getNestedJarFile(final JarEntryData sourceEntry,
|
||||||
|
JarEntryFilter... filters) throws IOException {
|
||||||
|
if (sourceEntry.isDirectory()) {
|
||||||
|
return getNestedJarFileFromDirectoryEntry(sourceEntry, filters);
|
||||||
|
}
|
||||||
|
return getNestedJarFileFromFileEntry(sourceEntry, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile getNestedJarFileFromDirectoryEntry(JarEntryData sourceEntry,
|
||||||
|
JarEntryFilter... filters) throws IOException {
|
||||||
|
final AsciiBytes sourceName = sourceEntry.getName();
|
||||||
|
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1];
|
||||||
|
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
|
||||||
|
filtersToUse[0] = new JarEntryFilter() {
|
||||||
|
@Override
|
||||||
|
public AsciiBytes apply(AsciiBytes name, JarEntryData entryData) {
|
||||||
|
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
||||||
|
return name.substring(sourceName.length());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new JarFile(this.rootFile, getName() + "!/"
|
||||||
|
+ sourceEntry.getName().substring(0, sourceName.length() - 1), this.data,
|
||||||
|
filtersToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarFile getNestedJarFileFromFileEntry(JarEntryData sourceEntry,
|
||||||
|
JarEntryFilter... filters) throws IOException {
|
||||||
|
if (sourceEntry.getMethod() != ZipEntry.STORED) {
|
||||||
|
throw new IllegalStateException("Unable to open nested compressed entry "
|
||||||
|
+ sourceEntry.getName());
|
||||||
|
}
|
||||||
|
return new JarFile(this.rootFile, getName() + "!/" + sourceEntry.getName(),
|
||||||
|
sourceEntry.getData(), filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new jar based on the filtered contents of this file.
|
||||||
|
* @param filters the set of jar entry filters to be applied
|
||||||
|
* @return a filtered {@link JarFile}
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public synchronized JarFile getFilteredJarFile(JarEntryFilter... filters)
|
||||||
|
throws IOException {
|
||||||
|
return new JarFile(this.rootFile, getName(), this.data, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JarEntry getContainedEntry(ZipEntry zipEntry) throws IOException {
|
||||||
|
if (zipEntry instanceof JarEntry
|
||||||
|
&& ((JarEntry) zipEntry).getSource().getSource() == this) {
|
||||||
|
return (JarEntry) zipEntry;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("ZipEntry must be contained in this file");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return (int) this.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
this.rootFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
|
||||||
|
* cannot be serialized and or cloned.
|
||||||
|
* @return the URL
|
||||||
|
* @throws MalformedURLException
|
||||||
|
*/
|
||||||
|
public URL getUrl() throws MalformedURLException {
|
||||||
|
JarURLStreamHandler handler = new JarURLStreamHandler(this);
|
||||||
|
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class JarURLConnection extends java.net.JarURLConnection {
|
||||||
|
|
||||||
|
private static final String JAR_URL_POSTFIX = "!/";
|
||||||
|
|
||||||
|
private static final String JAR_URL_PREFIX = "jar:file:";
|
||||||
|
|
||||||
|
private JarFile jarFile;
|
||||||
|
|
||||||
|
private JarEntryData jarEntryData;
|
||||||
|
|
||||||
|
private String jarEntryName;
|
||||||
|
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
|
||||||
|
super(new URL(buildRootUrl(jarFile)));
|
||||||
|
this.jarFile = jarFile;
|
||||||
|
|
||||||
|
String spec = url.getFile();
|
||||||
|
int separator = spec.lastIndexOf(JAR_URL_POSTFIX);
|
||||||
|
if (separator == -1) {
|
||||||
|
throw new MalformedURLException("no !/ found in url spec:" + spec);
|
||||||
|
}
|
||||||
|
if (separator + 2 != spec.length()) {
|
||||||
|
this.jarEntryName = spec.substring(separator + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect() throws IOException {
|
||||||
|
if (this.jarEntryName != null) {
|
||||||
|
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName);
|
||||||
|
if (this.jarEntryData == null) {
|
||||||
|
throw new FileNotFoundException("JAR entry " + this.jarEntryName
|
||||||
|
+ " not found in " + this.jarFile.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarFile getJarFile() throws IOException {
|
||||||
|
connect();
|
||||||
|
return this.jarFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarEntry getJarEntry() throws IOException {
|
||||||
|
connect();
|
||||||
|
return (this.jarEntryData == null ? null : this.jarEntryData.asJarEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
connect();
|
||||||
|
if (this.jarEntryName == null) {
|
||||||
|
throw new IOException("no entry name specified");
|
||||||
|
}
|
||||||
|
return this.jarEntryData.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentLength() {
|
||||||
|
try {
|
||||||
|
connect();
|
||||||
|
return this.jarEntryData == null ? this.jarFile.size() : this.jarEntryData
|
||||||
|
.getSize();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContent() throws IOException {
|
||||||
|
connect();
|
||||||
|
return (this.jarEntryData == null ? this.jarFile : super.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
if (this.contentType == null) {
|
||||||
|
// Guess the content type, don't bother with steams as mark is not
|
||||||
|
// supported
|
||||||
|
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
|
||||||
|
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
|
||||||
|
: this.contentType);
|
||||||
|
this.contentType = (this.contentType == null ? "content/unknown"
|
||||||
|
: this.contentType);
|
||||||
|
}
|
||||||
|
return this.contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildRootUrl(JarFile jarFile) {
|
||||||
|
String path = jarFile.getRootJarFile().getFile().getPath();
|
||||||
|
StringBuilder builder = new StringBuilder(JAR_URL_PREFIX.length() + path.length()
|
||||||
|
+ JAR_URL_POSTFIX.length());
|
||||||
|
builder.append(JAR_URL_PREFIX);
|
||||||
|
builder.append(path);
|
||||||
|
builder.append(JAR_URL_POSTFIX);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2013 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.loader.jar;
|
|
||||||
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.PushbackInputStream;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarInputStream;
|
|
||||||
|
|
||||||
import org.springframework.boot.loader.data.RandomAccessData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link JarInputStream} backed by {@link RandomAccessData}. Parsed entries provide
|
|
||||||
* access to the underlying data {@link RandomAccessData#getSubsection(long, long)
|
|
||||||
* subsection}.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
public class RandomAccessDataJarInputStream extends JarInputStream {
|
|
||||||
|
|
||||||
private RandomAccessData data;
|
|
||||||
|
|
||||||
private TrackingInputStream trackingInputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new {@link RandomAccessData} instance.
|
|
||||||
* @param data the source of the zip stream
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public RandomAccessDataJarInputStream(RandomAccessData data) throws IOException {
|
|
||||||
this(data, new TrackingInputStream(data.getInputStream()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor used so that we can call the super constructor with a
|
|
||||||
* {@link TrackingInputStream}.
|
|
||||||
* @param data the source of the zip stream
|
|
||||||
* @param trackingInputStream a tracking input stream
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private RandomAccessDataJarInputStream(RandomAccessData data,
|
|
||||||
TrackingInputStream trackingInputStream) throws IOException {
|
|
||||||
super(trackingInputStream);
|
|
||||||
this.data = data;
|
|
||||||
this.trackingInputStream = trackingInputStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RandomAccessDataJarEntry getNextEntry() throws IOException {
|
|
||||||
JarEntry entry = (JarEntry) super.getNextEntry();
|
|
||||||
if (entry == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int start = getPosition();
|
|
||||||
closeEntry();
|
|
||||||
int end = getPosition();
|
|
||||||
RandomAccessData entryData = this.data.getSubsection(start, end - start);
|
|
||||||
return new RandomAccessDataJarEntry(entry, entryData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getPosition() throws IOException {
|
|
||||||
int pushback = ((PushbackInputStream) this.in).available();
|
|
||||||
return this.trackingInputStream.getPosition() - pushback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal stream that tracks reads to provide a position.
|
|
||||||
*/
|
|
||||||
private static class TrackingInputStream extends FilterInputStream {
|
|
||||||
|
|
||||||
private int position = 0;
|
|
||||||
|
|
||||||
protected TrackingInputStream(InputStream in) {
|
|
||||||
super(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
return moveOn(super.read(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return moveOn(super.read(b), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
return moveOn(super.read(b, off, len), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int moveOn(int amount, boolean singleByteRead) {
|
|
||||||
this.position += (amount == -1 ? 0 : (singleByteRead ? 1 : amount));
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() throws IOException {
|
|
||||||
// Always return 0 so that we can accurately use PushbackInputStream.available
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPosition() {
|
|
||||||
return this.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,510 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2013 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.boot.loader.jar;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.JarURLConnection;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.net.URLStreamHandler;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.jar.JarEntry;
|
|
||||||
import java.util.jar.JarFile;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
import java.util.zip.Inflater;
|
|
||||||
import java.util.zip.InflaterInputStream;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
|
|
||||||
import org.springframework.boot.loader.data.ByteArrayRandomAccessData;
|
|
||||||
import org.springframework.boot.loader.data.RandomAccessData;
|
|
||||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Jar file that can loaded from a {@link RandomAccessDataFile}. This class extends and
|
|
||||||
* behaves in the same was a the standard JDK {@link JarFile} the following additional
|
|
||||||
* functionality.
|
|
||||||
* <ul>
|
|
||||||
* <li>Jar entries can be {@link JarEntryFilter filtered} during construction and new
|
|
||||||
* filtered files can be {@link #getFilteredJarFile(JarEntryFilter...) created} from
|
|
||||||
* existing files.</li>
|
|
||||||
* <li>A nested {@link JarFile} can be
|
|
||||||
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} based on any directory
|
|
||||||
* entry.</li>
|
|
||||||
* <li>A nested {@link JarFile} can be
|
|
||||||
* {@link #getNestedJarFile(ZipEntry, JarEntryFilter...) obtained} for embedded JAR files
|
|
||||||
* (as long as their entry is not compressed).</li>
|
|
||||||
* <li>Entry data can be accessed as {@link RandomAccessData}.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
public class RandomAccessJarFile extends JarFile {
|
|
||||||
|
|
||||||
private static final RandomAccessData EMPTY_DATA = new ByteArrayRandomAccessData(
|
|
||||||
new byte[0]);
|
|
||||||
|
|
||||||
private final RandomAccessDataFile rootJarFile;
|
|
||||||
|
|
||||||
private RandomAccessData data;
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
private final long size;
|
|
||||||
|
|
||||||
private Map<String, JarEntry> entries = new LinkedHashMap<String, JarEntry>();
|
|
||||||
|
|
||||||
private Manifest manifest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new {@link RandomAccessJarFile} backed by the specified file.
|
|
||||||
* @param file the root jar file
|
|
||||||
* @param filters an optional set of jar entry filters
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public RandomAccessJarFile(File file, JarEntryFilter... filters) throws IOException {
|
|
||||||
this(new RandomAccessDataFile(file), filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new {@link RandomAccessJarFile} backed by the specified file.
|
|
||||||
* @param file the root jar file
|
|
||||||
* @param filters an optional set of jar entry filters
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public RandomAccessJarFile(RandomAccessDataFile file, JarEntryFilter... filters)
|
|
||||||
throws IOException {
|
|
||||||
this(file, file.getFile().getPath(), file, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor used to create a new {@link RandomAccessJarFile} either
|
|
||||||
* directly or from a nested entry.
|
|
||||||
* @param rootJarFile the root jar file
|
|
||||||
* @param name the name of this file
|
|
||||||
* @param data the underlying data
|
|
||||||
* @param filters an optional set of jar entry filters
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private RandomAccessJarFile(RandomAccessDataFile rootJarFile, String name,
|
|
||||||
RandomAccessData data, JarEntryFilter... filters) throws IOException {
|
|
||||||
super(rootJarFile.getFile());
|
|
||||||
this.rootJarFile = rootJarFile;
|
|
||||||
this.name = name;
|
|
||||||
this.data = data;
|
|
||||||
this.size = data.getSize();
|
|
||||||
|
|
||||||
RandomAccessDataJarInputStream inputStream = new RandomAccessDataJarInputStream(
|
|
||||||
data);
|
|
||||||
try {
|
|
||||||
RandomAccessDataJarEntry zipEntry = inputStream.getNextEntry();
|
|
||||||
while (zipEntry != null) {
|
|
||||||
addJarEntry(zipEntry, filters);
|
|
||||||
zipEntry = inputStream.getNextEntry();
|
|
||||||
}
|
|
||||||
this.manifest = inputStream.getManifest();
|
|
||||||
if (this.manifest != null) {
|
|
||||||
addManifestEntries(filters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addManifestEntries(JarEntryFilter... filters) throws IOException {
|
|
||||||
|
|
||||||
Map<String, JarEntry> originalEntries = this.entries;
|
|
||||||
this.entries = new LinkedHashMap<String, JarEntry>();
|
|
||||||
|
|
||||||
ZipInputStream zipInputStream = new ZipInputStream(this.data.getInputStream());
|
|
||||||
try {
|
|
||||||
JarEntry entry;
|
|
||||||
do {
|
|
||||||
entry = new JarEntry(zipInputStream.getNextEntry());
|
|
||||||
entry.setMethod(ZipEntry.STORED);
|
|
||||||
RandomAccessData data = EMPTY_DATA;
|
|
||||||
if (MANIFEST_NAME.equals(entry.getName())) {
|
|
||||||
ByteArrayOutputStream manifestBytes = new ByteArrayOutputStream();
|
|
||||||
this.manifest.write(manifestBytes);
|
|
||||||
manifestBytes.close();
|
|
||||||
data = new ByteArrayRandomAccessData(manifestBytes.toByteArray());
|
|
||||||
}
|
|
||||||
addJarEntry(new RandomAccessDataJarEntry(entry, data), filters);
|
|
||||||
}
|
|
||||||
while (!MANIFEST_NAME.equals(entry.getName()));
|
|
||||||
|
|
||||||
this.entries.putAll(originalEntries);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
zipInputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addJarEntry(RandomAccessDataJarEntry zipEntry, JarEntryFilter... filters) {
|
|
||||||
Entry jarEntry = new Entry(zipEntry);
|
|
||||||
String name = zipEntry.getName();
|
|
||||||
for (JarEntryFilter filter : filters) {
|
|
||||||
name = (filter == null || name == null ? name : filter.apply(name, jarEntry));
|
|
||||||
}
|
|
||||||
if (name != null) {
|
|
||||||
jarEntry.setName(name);
|
|
||||||
this.entries.put(name, jarEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final RandomAccessDataFile getRootJarFile() {
|
|
||||||
return this.rootJarFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Manifest getManifest() throws IOException {
|
|
||||||
return this.manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Enumeration<JarEntry> entries() {
|
|
||||||
return Collections.enumeration(this.entries.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JarEntry getJarEntry(String name) {
|
|
||||||
return (JarEntry) getEntry(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ZipEntry getEntry(String name) {
|
|
||||||
JarEntry entry = this.entries.get(name);
|
|
||||||
if (entry == null && name != null && !name.endsWith("/")) {
|
|
||||||
entry = this.entries.get(name + "/");
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
|
||||||
InputStream inputStream = getData(ze).getInputStream();
|
|
||||||
if (ze.getMethod() == ZipEntry.DEFLATED) {
|
|
||||||
inputStream = new ZipInflaterInputStream(inputStream, (int) ze.getSize());
|
|
||||||
}
|
|
||||||
return inputStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a nested {@link RandomAccessJarFile} loaded from the specified entry.
|
|
||||||
* @param ze the zip entry
|
|
||||||
* @param filters an optional set of jar entry filters to be applied
|
|
||||||
* @return a {@link RandomAccessJarFile} for the entry
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public synchronized RandomAccessJarFile getNestedJarFile(final ZipEntry ze,
|
|
||||||
JarEntryFilter... filters) throws IOException {
|
|
||||||
if (ze == null) {
|
|
||||||
throw new IllegalArgumentException("ZipEntry must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ze.isDirectory()) {
|
|
||||||
return getNestedJarFileFromDirectoryEntry(ze, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getNestedJarFileFromFileEntry(ze, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RandomAccessJarFile getNestedJarFileFromDirectoryEntry(final ZipEntry entry,
|
|
||||||
JarEntryFilter... filters) throws IOException {
|
|
||||||
final String name = entry.getName();
|
|
||||||
JarEntryFilter[] filtersToUse = new JarEntryFilter[filters.length + 1];
|
|
||||||
System.arraycopy(filters, 0, filtersToUse, 1, filters.length);
|
|
||||||
filtersToUse[0] = new JarEntryFilter() {
|
|
||||||
@Override
|
|
||||||
public String apply(String entryName, JarEntry ze) {
|
|
||||||
if (entryName.startsWith(name) && !entryName.equals(name)) {
|
|
||||||
return entryName.substring(entry.getName().length());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return new RandomAccessJarFile(this.rootJarFile, getName() + "!/"
|
|
||||||
+ name.substring(0, name.length() - 1), this.data, filtersToUse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RandomAccessJarFile getNestedJarFileFromFileEntry(ZipEntry entry,
|
|
||||||
JarEntryFilter... filters) throws IOException {
|
|
||||||
if (entry.getMethod() != ZipEntry.STORED) {
|
|
||||||
throw new IllegalStateException("Unable to open nested compressed entry "
|
|
||||||
+ entry.getName());
|
|
||||||
}
|
|
||||||
return new RandomAccessJarFile(this.rootJarFile, getName() + "!/"
|
|
||||||
+ entry.getName(), getData(entry), filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a new jar based on the filtered contents of this file.
|
|
||||||
* @param filters the set of jar entry filters to be applied
|
|
||||||
* @return a filtered {@link RandomAccessJarFile}
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public synchronized RandomAccessJarFile getFilteredJarFile(JarEntryFilter... filters)
|
|
||||||
throws IOException {
|
|
||||||
return new RandomAccessJarFile(this.rootJarFile, getName(), this.data, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return {@link RandomAccessData} for the specified entry.
|
|
||||||
* @param ze the zip entry
|
|
||||||
* @return the entry {@link RandomAccessData}
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private synchronized RandomAccessData getData(ZipEntry ze) throws IOException {
|
|
||||||
if (!this.entries.containsValue(ze)) {
|
|
||||||
throw new IllegalArgumentException("ZipEntry must be contained in this file");
|
|
||||||
}
|
|
||||||
return ((Entry) ze).getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return (int) this.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
this.rootJarFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
|
|
||||||
* cannot be serialized and or cloned.
|
|
||||||
* @return the URL
|
|
||||||
* @throws MalformedURLException
|
|
||||||
*/
|
|
||||||
public URL getUrl() throws MalformedURLException {
|
|
||||||
RandomAccessJarURLStreamHandler handler = new RandomAccessJarURLStreamHandler(
|
|
||||||
this);
|
|
||||||
return new URL("jar", "", -1, "file:" + getName() + "!/", handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A single {@link JarEntry} in this file.
|
|
||||||
*/
|
|
||||||
private static class Entry extends JarEntry {
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
private RandomAccessData entryData;
|
|
||||||
|
|
||||||
public Entry(RandomAccessDataJarEntry entry) {
|
|
||||||
super(entry);
|
|
||||||
this.entryData = entry.getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return (this.name == null ? super.getName() : this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RandomAccessData getData() {
|
|
||||||
return this.entryData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link URLStreamHandler} used to support {@link RandomAccessJarFile#getUrl()}.
|
|
||||||
*/
|
|
||||||
private static class RandomAccessJarURLStreamHandler extends URLStreamHandler {
|
|
||||||
|
|
||||||
private RandomAccessJarFile jarFile;
|
|
||||||
|
|
||||||
public RandomAccessJarURLStreamHandler(RandomAccessJarFile jarFile) {
|
|
||||||
this.jarFile = jarFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected URLConnection openConnection(URL url) throws IOException {
|
|
||||||
return new RandomAccessJarURLConnection(url, this.jarFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link JarURLConnection} used to support {@link RandomAccessJarFile#getUrl()}.
|
|
||||||
*/
|
|
||||||
private static class RandomAccessJarURLConnection extends JarURLConnection {
|
|
||||||
|
|
||||||
private RandomAccessJarFile jarFile;
|
|
||||||
|
|
||||||
private JarEntry jarEntry;
|
|
||||||
|
|
||||||
private String jarEntryName;
|
|
||||||
|
|
||||||
private String contentType;
|
|
||||||
|
|
||||||
protected RandomAccessJarURLConnection(URL url, RandomAccessJarFile jarFile)
|
|
||||||
throws MalformedURLException {
|
|
||||||
super(new URL("jar:file:" + jarFile.getRootJarFile().getFile().getPath()
|
|
||||||
+ "!/"));
|
|
||||||
this.jarFile = jarFile;
|
|
||||||
|
|
||||||
String spec = url.getFile();
|
|
||||||
int separator = spec.lastIndexOf("!/");
|
|
||||||
if (separator == -1) {
|
|
||||||
throw new MalformedURLException("no !/ found in url spec:" + spec);
|
|
||||||
}
|
|
||||||
if (separator + 2 != spec.length()) {
|
|
||||||
this.jarEntryName = spec.substring(separator + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect() throws IOException {
|
|
||||||
if (this.jarEntryName != null) {
|
|
||||||
this.jarEntry = this.jarFile.getJarEntry(this.jarEntryName);
|
|
||||||
if (this.jarEntry == null) {
|
|
||||||
throw new FileNotFoundException("JAR entry " + this.jarEntryName
|
|
||||||
+ " not found in " + this.jarFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RandomAccessJarFile getJarFile() throws IOException {
|
|
||||||
connect();
|
|
||||||
return this.jarFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JarEntry getJarEntry() throws IOException {
|
|
||||||
connect();
|
|
||||||
return this.jarEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream getInputStream() throws IOException {
|
|
||||||
connect();
|
|
||||||
if (this.jarEntryName == null) {
|
|
||||||
throw new IOException("no entry name specified");
|
|
||||||
}
|
|
||||||
return this.jarFile.getInputStream(this.jarEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getContentLength() {
|
|
||||||
try {
|
|
||||||
connect();
|
|
||||||
return (int) (this.jarEntry == null ? this.jarFile.size() : this.jarEntry
|
|
||||||
.getSize());
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getContent() throws IOException {
|
|
||||||
connect();
|
|
||||||
return (this.jarEntry == null ? this.jarFile : super.getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getContentType() {
|
|
||||||
if (this.contentType == null) {
|
|
||||||
// Guess the content type, don't bother with steams as mark is not
|
|
||||||
// supported
|
|
||||||
this.contentType = (this.jarEntryName == null ? "x-java/jar" : null);
|
|
||||||
this.contentType = (this.contentType == null ? guessContentTypeFromName(this.jarEntryName)
|
|
||||||
: this.contentType);
|
|
||||||
this.contentType = (this.contentType == null ? "content/unknown"
|
|
||||||
: this.contentType);
|
|
||||||
}
|
|
||||||
return this.contentType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte
|
|
||||||
* (which is required with JDK 6) and returns accurate available() results.
|
|
||||||
*/
|
|
||||||
private static class ZipInflaterInputStream extends InflaterInputStream {
|
|
||||||
|
|
||||||
private boolean extraBytesWritten;
|
|
||||||
|
|
||||||
private int available;
|
|
||||||
|
|
||||||
public ZipInflaterInputStream(InputStream inputStream, int size) {
|
|
||||||
super(inputStream, new Inflater(true), 512);
|
|
||||||
this.available = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() throws IOException {
|
|
||||||
if (this.available < 0) {
|
|
||||||
return super.available();
|
|
||||||
}
|
|
||||||
return this.available;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
int result = super.read(b, off, len);
|
|
||||||
if (result != -1) {
|
|
||||||
this.available -= result;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void fill() throws IOException {
|
|
||||||
try {
|
|
||||||
super.fill();
|
|
||||||
}
|
|
||||||
catch (EOFException ex) {
|
|
||||||
if (this.extraBytesWritten) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
this.len = 1;
|
|
||||||
this.buf[0] = 0x0;
|
|
||||||
this.extraBytesWritten = true;
|
|
||||||
this.inf.setInput(this.buf, 0, this.len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader.jar;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
|
||||||
|
* is required with JDK 6) and returns accurate available() results.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ZipInflaterInputStream extends InflaterInputStream {
|
||||||
|
|
||||||
|
private boolean extraBytesWritten;
|
||||||
|
|
||||||
|
private int available;
|
||||||
|
|
||||||
|
public ZipInflaterInputStream(InputStream inputStream, int size) {
|
||||||
|
super(inputStream, new Inflater(true), 512);
|
||||||
|
this.available = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
if (this.available < 0) {
|
||||||
|
return super.available();
|
||||||
|
}
|
||||||
|
return this.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
int result = super.read(b, off, len);
|
||||||
|
if (result != -1) {
|
||||||
|
this.available -= result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fill() throws IOException {
|
||||||
|
try {
|
||||||
|
super.fill();
|
||||||
|
}
|
||||||
|
catch (EOFException ex) {
|
||||||
|
if (this.extraBytesWritten) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
this.len = 1;
|
||||||
|
this.buf[0] = 0x0;
|
||||||
|
this.extraBytesWritten = true;
|
||||||
|
this.inf.setInput(this.buf, 0, this.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2013 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.loader;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link AsciiBytes}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class AsciiBytesTests {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFromBytes() throws Exception {
|
||||||
|
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
assertThat(bytes.toString(), equalTo("AB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFromBytesWithOffset() throws Exception {
|
||||||
|
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
assertThat(bytes.toString(), equalTo("BC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createFromString() throws Exception {
|
||||||
|
AsciiBytes bytes = new AsciiBytes("AB");
|
||||||
|
assertThat(bytes.toString(), equalTo("AB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void length() throws Exception {
|
||||||
|
AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
assertThat(b1.length(), equalTo(2));
|
||||||
|
assertThat(b2.length(), equalTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startWith() throws Exception {
|
||||||
|
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
|
||||||
|
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abc.startsWith(abc), equalTo(true));
|
||||||
|
assertThat(abc.startsWith(ab), equalTo(true));
|
||||||
|
assertThat(abc.startsWith(bc), equalTo(false));
|
||||||
|
assertThat(abc.startsWith(abcd), equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void endsWith() throws Exception {
|
||||||
|
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
|
||||||
|
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
|
||||||
|
AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 });
|
||||||
|
assertThat(abc.endsWith(abc), equalTo(true));
|
||||||
|
assertThat(abc.endsWith(bc), equalTo(true));
|
||||||
|
assertThat(abc.endsWith(ab), equalTo(false));
|
||||||
|
assertThat(abc.endsWith(aabc), equalTo(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void substringFromBeingIndex() throws Exception {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abcd.substring(0).toString(), equalTo("ABCD"));
|
||||||
|
assertThat(abcd.substring(1).toString(), equalTo("BCD"));
|
||||||
|
assertThat(abcd.substring(2).toString(), equalTo("CD"));
|
||||||
|
assertThat(abcd.substring(3).toString(), equalTo("D"));
|
||||||
|
assertThat(abcd.substring(4).toString(), equalTo(""));
|
||||||
|
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||||
|
abcd.substring(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void substring() throws Exception {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
assertThat(abcd.substring(0, 4).toString(), equalTo("ABCD"));
|
||||||
|
assertThat(abcd.substring(1, 3).toString(), equalTo("BC"));
|
||||||
|
assertThat(abcd.substring(3, 4).toString(), equalTo("D"));
|
||||||
|
assertThat(abcd.substring(3, 3).toString(), equalTo(""));
|
||||||
|
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||||
|
abcd.substring(3, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void appendString() throws Exception {
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
AsciiBytes appended = bc.append("D");
|
||||||
|
assertThat(bc.toString(), equalTo("BC"));
|
||||||
|
assertThat(appended.toString(), equalTo("BCD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void appendBytes() throws Exception {
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||||
|
AsciiBytes appended = bc.append(new byte[] { 68 });
|
||||||
|
assertThat(bc.toString(), equalTo("BC"));
|
||||||
|
assertThat(appended.toString(), equalTo("BCD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hashCodeAndEquals() throws Exception {
|
||||||
|
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||||
|
AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 });
|
||||||
|
AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 })
|
||||||
|
.substring(1, 3);
|
||||||
|
AsciiBytes bc_string = new AsciiBytes("BC");
|
||||||
|
assertThat(bc.hashCode(), equalTo(bc.hashCode()));
|
||||||
|
assertThat(bc.hashCode(), equalTo(bc_substring.hashCode()));
|
||||||
|
assertThat(bc.hashCode(), equalTo(bc_string.hashCode()));
|
||||||
|
assertThat(bc, equalTo(bc));
|
||||||
|
assertThat(bc, equalTo(bc_substring));
|
||||||
|
assertThat(bc, equalTo(bc_string));
|
||||||
|
|
||||||
|
assertThat(bc.hashCode(), not(equalTo(abcd.hashCode())));
|
||||||
|
assertThat(bc, not(equalTo(abcd)));
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue