Reduce garbage created when loading fat jars
Refactor fat jar loader classes so that less `char[]` instances are created. This is primarily achieved by adding a new `StringSequence` class that can chop up Strings without needing to copy the underlying array. Since Java 8, calls to `String.subString(...)` always copy the underlying char array. For many of the operations that we need, this is unnecessary. Fixes gh-11405
This commit is contained in:
parent
c024313141
commit
aa66d5dfb8
|
|
@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets;
|
|||
*/
|
||||
final class AsciiBytes {
|
||||
|
||||
private static final int[] EXCESS = { 0x0, 0x1080, 0x96, 0x1c82080 };
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
private final int offset;
|
||||
|
|
@ -118,38 +120,58 @@ final class AsciiBytes {
|
|||
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
|
||||
}
|
||||
|
||||
public AsciiBytes append(String string) {
|
||||
if (string == null || string.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return append(string.getBytes(StandardCharsets.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;
|
||||
}
|
||||
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, StandardCharsets.UTF_8);
|
||||
this.string = new String(this.bytes, this.offset, this.length,
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
return this.string;
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence name, char suffix) {
|
||||
int charIndex = 0;
|
||||
int nameLen = name.length();
|
||||
int totalLen = (nameLen + (suffix == 0 ? 0 : 1));
|
||||
for (int i = this.offset; i < this.offset + this.length; i++) {
|
||||
int b = this.bytes[i];
|
||||
if (b < 0) {
|
||||
b = b & 0x7F;
|
||||
int limit = getRemainingUtfBytes(b);
|
||||
for (int j = 0; j < limit; j++) {
|
||||
b = (b << 6) + (this.bytes[++i] & 0xFF);
|
||||
}
|
||||
b -= EXCESS[limit];
|
||||
}
|
||||
char c = getChar(name, suffix, charIndex++);
|
||||
if (b <= 0xFFFF) {
|
||||
if (c != b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (c != ((b >> 0xA) + 0xD7C0)) {
|
||||
return false;
|
||||
}
|
||||
c = getChar(name, suffix, charIndex++);
|
||||
if (c != ((b & 0x3FF) + 0xDC00)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return charIndex == totalLen;
|
||||
}
|
||||
|
||||
private char getChar(CharSequence name, char suffix, int index) {
|
||||
if (index < name.length()) {
|
||||
return name.charAt(index);
|
||||
}
|
||||
if (index == name.length()) {
|
||||
return suffix;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = this.hash;
|
||||
|
|
@ -158,24 +180,11 @@ final class AsciiBytes {
|
|||
int b = this.bytes[i];
|
||||
if (b < 0) {
|
||||
b = b & 0x7F;
|
||||
int limit;
|
||||
int excess = 0x80;
|
||||
if (b < 96) {
|
||||
limit = 1;
|
||||
excess += 0x40 << 6;
|
||||
}
|
||||
else if (b < 112) {
|
||||
limit = 2;
|
||||
excess += (0x60 << 12) + (0x80 << 6);
|
||||
}
|
||||
else {
|
||||
limit = 3;
|
||||
excess += (0x70 << 18) + (0x80 << 12) + (0x80 << 6);
|
||||
}
|
||||
int limit = getRemainingUtfBytes(b);
|
||||
for (int j = 0; j < limit; j++) {
|
||||
b = (b << 6) + (this.bytes[++i] & 0xFF);
|
||||
}
|
||||
b -= excess;
|
||||
b -= EXCESS[limit];
|
||||
}
|
||||
if (b <= 0xFFFF) {
|
||||
hash = 31 * hash + b;
|
||||
|
|
@ -190,6 +199,10 @@ final class AsciiBytes {
|
|||
return hash;
|
||||
}
|
||||
|
||||
private int getRemainingUtfBytes(int b) {
|
||||
return (b < 96 ? 1 : (b < 112 ? 2 : 3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
|
|
@ -216,16 +229,17 @@ final class AsciiBytes {
|
|||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static int hashCode(String string) {
|
||||
// We're compatible with String's hashCode().
|
||||
return string.hashCode();
|
||||
public static int hashCode(CharSequence charSequence) {
|
||||
// We're compatible with String's hashCode()
|
||||
if (charSequence instanceof StringSequence) {
|
||||
// ... but save making an unnecessary String for StringSequence
|
||||
return charSequence.hashCode();
|
||||
}
|
||||
return charSequence.toString().hashCode();
|
||||
}
|
||||
|
||||
public static int hashCode(int hash, String string) {
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
hash = 31 * hash + string.charAt(i);
|
||||
}
|
||||
return hash;
|
||||
public static int hashCode(int hash, char suffix) {
|
||||
return (suffix == 0 ? hash : (31 * hash + suffix));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ final class CentralDirectoryFileHeader implements FileHeader {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName(String name, String suffix) {
|
||||
return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix));
|
||||
public boolean hasName(CharSequence name, char suffix) {
|
||||
return this.name.matches(name, suffix);
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ interface FileHeader {
|
|||
/**
|
||||
* Returns {@code true} if the header has the given name.
|
||||
* @param name the name to test
|
||||
* @param suffix an additional suffix (or {@code null})
|
||||
* @param suffix an additional suffix (or {@code 0})
|
||||
* @return {@code true} if the header has the given name
|
||||
*/
|
||||
boolean hasName(String name, String suffix);
|
||||
boolean hasName(CharSequence name, char suffix);
|
||||
|
||||
/**
|
||||
* Return the offset of the load file header within the archive data.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import java.util.jar.Manifest;
|
|||
*/
|
||||
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
||||
|
||||
private final AsciiBytes name;
|
||||
|
||||
private Certificate[] certificates;
|
||||
|
||||
private CodeSigner[] codeSigners;
|
||||
|
|
@ -41,6 +43,7 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
|||
|
||||
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
|
||||
super(header.getName().toString());
|
||||
this.name = header.getName();
|
||||
this.jarFile = jarFile;
|
||||
this.localHeaderOffset = header.getLocalHeaderOffset();
|
||||
setCompressedSize(header.getCompressedSize());
|
||||
|
|
@ -53,10 +56,13 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
|||
setTime(header.getTime());
|
||||
}
|
||||
|
||||
AsciiBytes getAsciiBytesName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName(String name, String suffix) {
|
||||
return getName().length() == name.length() + suffix.length()
|
||||
&& getName().startsWith(name) && getName().endsWith(suffix);
|
||||
public boolean hasName(CharSequence name, char suffix) {
|
||||
return this.name.matches(name, suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -191,6 +191,10 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
};
|
||||
}
|
||||
|
||||
public JarEntry getJarEntry(CharSequence name) {
|
||||
return this.entries.getEntry(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getJarEntry(String name) {
|
||||
return (JarEntry) getEntry(name);
|
||||
|
|
@ -228,8 +232,7 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
* @return a {@link JarFile} for the entry
|
||||
* @throws IOException if the nested jar file cannot be read
|
||||
*/
|
||||
public synchronized JarFile getNestedJarFile(ZipEntry entry)
|
||||
throws IOException {
|
||||
public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
|
||||
return getNestedJarFile((JarEntry) entry);
|
||||
}
|
||||
|
||||
|
|
@ -257,16 +260,16 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
}
|
||||
|
||||
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
|
||||
final AsciiBytes sourceName = new AsciiBytes(entry.getName());
|
||||
JarEntryFilter filter = (name) -> {
|
||||
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
|
||||
return name.substring(sourceName.length());
|
||||
AsciiBytes name = entry.getAsciiBytesName();
|
||||
JarEntryFilter filter = (candidate) -> {
|
||||
if (candidate.startsWith(name) && !candidate.equals(name)) {
|
||||
return candidate.substring(name.length());
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return new JarFile(this.rootFile,
|
||||
this.pathFromRoot + "!/"
|
||||
+ entry.getName().substring(0, sourceName.length() - 1),
|
||||
+ entry.getName().substring(0, name.length() - 1),
|
||||
this.data, filter, JarFileType.NESTED_DIRECTORY);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
|
||||
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
||||
|
||||
private static final String SLASH = "/";
|
||||
private static final char SLASH = '/';
|
||||
|
||||
private static final String NO_SUFFIX = "";
|
||||
private static final char NO_SUFFIX = 0;
|
||||
|
||||
protected static final int ENTRY_CACHE_SIZE = 25;
|
||||
|
||||
|
|
@ -166,11 +166,11 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
return new EntryIterator();
|
||||
}
|
||||
|
||||
public boolean containsEntry(String name) {
|
||||
public boolean containsEntry(CharSequence name) {
|
||||
return getEntry(name, FileHeader.class, true) != null;
|
||||
}
|
||||
|
||||
public JarEntry getEntry(String name) {
|
||||
public JarEntry getEntry(CharSequence name) {
|
||||
return getEntry(name, JarEntry.class, true);
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
+ nameLength + extraLength, entry.getCompressedSize());
|
||||
}
|
||||
|
||||
private <T extends FileHeader> T getEntry(String name, Class<T> type,
|
||||
private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type,
|
||||
boolean cacheEntry) {
|
||||
int hashCode = AsciiBytes.hashCode(name);
|
||||
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
|
||||
|
|
@ -224,8 +224,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
return entry;
|
||||
}
|
||||
|
||||
private <T extends FileHeader> T getEntry(int hashCode, String name, String suffix,
|
||||
Class<T> type, boolean cacheEntry) {
|
||||
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name,
|
||||
char suffix, Class<T> type, boolean cacheEntry) {
|
||||
int index = getFirstIndex(hashCode);
|
||||
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
|
||||
T entry = getEntry(index, type, cacheEntry);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
|
||||
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(
|
||||
new StringSequence(""));
|
||||
|
||||
private static final String READ_ACTION = "read";
|
||||
|
||||
|
|
@ -254,17 +255,17 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
}
|
||||
|
||||
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
|
||||
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
|
||||
StringSequence spec = new StringSequence(url.getFile());
|
||||
int index = indexOfRootSpec(spec, jarFile.getPathFromRoot());
|
||||
int separator;
|
||||
int index = 0;
|
||||
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
|
||||
String entryName = spec.substring(index, separator);
|
||||
StringSequence entryName = spec.subSequence(index, separator);
|
||||
JarEntry jarEntry = jarFile.getJarEntry(entryName);
|
||||
if (jarEntry == null) {
|
||||
return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName));
|
||||
}
|
||||
jarFile = jarFile.getNestedJarFile(jarEntry);
|
||||
index += separator + SEPARATOR.length();
|
||||
index = separator + SEPARATOR.length();
|
||||
}
|
||||
JarEntryName jarEntryName = JarEntryName.get(spec, index);
|
||||
if (Boolean.TRUE.equals(useFastExceptions.get())) {
|
||||
|
|
@ -276,14 +277,12 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
return new JarURLConnection(url, jarFile, jarEntryName);
|
||||
}
|
||||
|
||||
private static String extractFullSpec(URL url, String pathFromRoot) {
|
||||
String file = url.getFile();
|
||||
private static int indexOfRootSpec(StringSequence file, String pathFromRoot) {
|
||||
int separatorIndex = file.indexOf(SEPARATOR);
|
||||
if (separatorIndex < 0) {
|
||||
return "";
|
||||
return -1;
|
||||
}
|
||||
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length();
|
||||
return file.substring(specIndex);
|
||||
return separatorIndex + SEPARATOR.length() + pathFromRoot.length();
|
||||
}
|
||||
|
||||
private static JarURLConnection notFound() {
|
||||
|
|
@ -308,22 +307,22 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
*/
|
||||
static class JarEntryName {
|
||||
|
||||
private final String name;
|
||||
private final StringSequence name;
|
||||
|
||||
private String contentType;
|
||||
|
||||
JarEntryName(String spec) {
|
||||
JarEntryName(StringSequence spec) {
|
||||
this.name = decode(spec);
|
||||
}
|
||||
|
||||
private String decode(String source) {
|
||||
private StringSequence decode(StringSequence source) {
|
||||
if (source.isEmpty() || (source.indexOf('%') < 0)) {
|
||||
return source;
|
||||
}
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
|
||||
write(source, bos);
|
||||
write(source.toString(), bos);
|
||||
// AsciiBytes is what is used to store the JarEntries so make it symmetric
|
||||
return AsciiBytes.toString(bos.toByteArray());
|
||||
return new StringSequence(AsciiBytes.toString(bos.toByteArray()));
|
||||
}
|
||||
|
||||
private void write(String source, ByteArrayOutputStream outputStream) {
|
||||
|
|
@ -367,7 +366,7 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
return this.name.toString();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
|
|
@ -389,15 +388,15 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
return type;
|
||||
}
|
||||
|
||||
public static JarEntryName get(String spec) {
|
||||
public static JarEntryName get(StringSequence spec) {
|
||||
return get(spec, 0);
|
||||
}
|
||||
|
||||
public static JarEntryName get(String spec, int beginIndex) {
|
||||
public static JarEntryName get(StringSequence spec, int beginIndex) {
|
||||
if (spec.length() <= beginIndex) {
|
||||
return EMPTY_JAR_ENTRY_NAME;
|
||||
}
|
||||
return new JarEntryName(spec.substring(beginIndex));
|
||||
return new JarEntryName(spec.subSequence(beginIndex));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.util.Objects;
|
||||
|
||||
/**
|
||||
* A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular
|
||||
* {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying
|
||||
* character array.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class StringSequence implements CharSequence {
|
||||
|
||||
private final String source;
|
||||
|
||||
private final int start;
|
||||
|
||||
private final int end;
|
||||
|
||||
private int hash;
|
||||
|
||||
StringSequence(String source) {
|
||||
this(source, 0, (source == null ? -1 : source.length()));
|
||||
}
|
||||
|
||||
StringSequence(String source, int start, int end) {
|
||||
Objects.requireNonNull(source, "Source must not be null");
|
||||
if (start < 0) {
|
||||
throw new StringIndexOutOfBoundsException(start);
|
||||
}
|
||||
if (end > source.length()) {
|
||||
throw new StringIndexOutOfBoundsException(end);
|
||||
}
|
||||
this.source = source;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public StringSequence subSequence(int start) {
|
||||
return subSequence(start, length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringSequence subSequence(int start, int end) {
|
||||
int subSequenceStart = this.start + start;
|
||||
int subSequenceEnd = this.start + end;
|
||||
if (subSequenceStart > this.end) {
|
||||
throw new StringIndexOutOfBoundsException(start);
|
||||
}
|
||||
if (subSequenceEnd > this.end) {
|
||||
throw new StringIndexOutOfBoundsException(end);
|
||||
}
|
||||
return new StringSequence(this.source, subSequenceStart, subSequenceEnd);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return length() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return this.end - this.start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return this.source.charAt(this.start + index);
|
||||
}
|
||||
|
||||
public int indexOf(char ch) {
|
||||
return this.source.indexOf(ch, this.start) - this.start;
|
||||
}
|
||||
|
||||
public int indexOf(String str) {
|
||||
return this.source.indexOf(str, this.start) - this.start;
|
||||
}
|
||||
|
||||
public int indexOf(String str, int fromIndex) {
|
||||
return this.source.indexOf(str, this.start + fromIndex) - this.start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.source.substring(this.start, this.end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = this.hash;
|
||||
if (hash == 0 && length() > 0) {
|
||||
for (int i = this.start; i < this.end; i++) {
|
||||
hash = 31 * hash + this.source.charAt(i);
|
||||
}
|
||||
this.hash = hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
StringSequence other = (StringSequence) obj;
|
||||
int n = length();
|
||||
if (n == other.length()) {
|
||||
int i = 0;
|
||||
while (n-- != 0) {
|
||||
if (charAt(i) != other.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
*/
|
||||
public class AsciiBytesTests {
|
||||
|
||||
private static final char NO_SUFFIX = 0;
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
|
|
@ -106,22 +108,6 @@ public class AsciiBytesTests {
|
|||
abcd.substring(3, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendString() {
|
||||
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||
AsciiBytes appended = bc.append("D");
|
||||
assertThat(bc.toString()).isEqualTo("BC");
|
||||
assertThat(appended.toString()).isEqualTo("BCD");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendBytes() {
|
||||
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
|
||||
AsciiBytes appended = bc.append(new byte[] { 68 });
|
||||
assertThat(bc.toString()).isEqualTo("BC");
|
||||
assertThat(appended.toString()).isEqualTo("BCD");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCodeAndEquals() {
|
||||
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
|
||||
|
|
@ -163,4 +149,42 @@ public class AsciiBytesTests {
|
|||
assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesSameAsString() {
|
||||
matchesSameAsString("abcABC123xyz!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesSameAsStringWithSpecial() {
|
||||
matchesSameAsString("special/\u00EB.dat");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesSameAsStringWithCyrillicCharacters() {
|
||||
matchesSameAsString("\u0432\u0435\u0441\u043D\u0430");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesDifferentLengths() {
|
||||
assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse();
|
||||
assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse();
|
||||
assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue();
|
||||
assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse();
|
||||
assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse();
|
||||
assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesSuffix() {
|
||||
assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesSameAsStringWithEmoji() {
|
||||
matchesSameAsString("\ud83d\udca9");
|
||||
}
|
||||
|
||||
private void matchesSameAsString(String input) {
|
||||
assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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 org.junit.Test;
|
||||
|
||||
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JarEntryName}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class JarEntryNameTests {
|
||||
|
||||
@Test
|
||||
public void basicName() {
|
||||
assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithSingleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName("%61/%62/%43.class").toString())
|
||||
.isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithDoubleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName("%c3%a1/b/C.class").toString())
|
||||
.isEqualTo("\u00e1/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
|
||||
assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString())
|
||||
.isEqualTo("\u00e1/b/\u00c7.class");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import org.junit.Test;
|
|||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import org.springframework.boot.loader.TestJarCreator;
|
||||
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
|
@ -155,6 +156,31 @@ public class JarURLConnectionTests {
|
|||
.isEqualTo(connection.getJarEntry().getTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarEntryBasicName() {
|
||||
assertThat(new JarEntryName(new StringSequence("a/b/C.class")).toString())
|
||||
.isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarEntryNameWithSingleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class")).toString())
|
||||
.isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarEntryNameWithDoubleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class")).toString())
|
||||
.isEqualTo("\u00e1/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
|
||||
assertThat(
|
||||
new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class")).toString())
|
||||
.isEqualTo("\u00e1/b/\u00c7.class");
|
||||
}
|
||||
|
||||
private String getAbsolutePath() {
|
||||
return this.rootJarFile.getAbsolutePath().replace('\\', '/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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 org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link StringSequence}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class StringSequenceTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void createWhenSourceIsNullShouldThrowException() {
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Source must not be null");
|
||||
new StringSequence(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWithIndexWhenSourceIsNullShouldThrowException() {
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Source must not be null");
|
||||
new StringSequence(null, 0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenStartIsLessThanZeroShouldThrowException() {
|
||||
this.thrown.expect(StringIndexOutOfBoundsException.class);
|
||||
new StringSequence("x", -1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenEndIsGreaterThanLengthShouldThrowException() {
|
||||
this.thrown.expect(StringIndexOutOfBoundsException.class);
|
||||
new StringSequence("x", 0, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatFromString() {
|
||||
assertThat(new StringSequence("test").toString()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWithJustStartShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("smiles").subSequence(1).toString())
|
||||
.isEqualTo("miles");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).toString())
|
||||
.isEqualTo("urge");
|
||||
assertThat(new StringSequence("smiles").subSequence(1, 5).toString())
|
||||
.isEqualTo("mile");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)
|
||||
.toString()).isEqualTo("rg");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenEndPastExistingEndShouldThrowException() {
|
||||
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||
assertThat(sequence.toString()).isEqualTo("bcd");
|
||||
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
sequence.subSequence(3, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenStartPastExistingEndShouldThrowException() {
|
||||
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||
assertThat(sequence.toString()).isEqualTo("bcd");
|
||||
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
sequence.subSequence(4, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmptyWhenEmptyShouldReturnTrue() {
|
||||
assertThat(new StringSequence("").isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmptyWhenNotEmptyShouldReturnFalse() {
|
||||
assertThat(new StringSequence("x").isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lengthShouldReturnLength() {
|
||||
StringSequence sequence = new StringSequence("hamburger");
|
||||
assertThat(sequence.length()).isEqualTo(9);
|
||||
assertThat(sequence.subSequence(4, 8).length()).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void charAtShouldReturnChar() {
|
||||
StringSequence sequence = new StringSequence("hamburger");
|
||||
assertThat(sequence.charAt(0)).isEqualTo('h');
|
||||
assertThat(sequence.charAt(1)).isEqualTo('a');
|
||||
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
|
||||
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfCharShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf('a')).isEqualTo(0);
|
||||
assertThat(sequence.indexOf('b')).isEqualTo(2);
|
||||
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfStringShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf("a")).isEqualTo(0);
|
||||
assertThat(sequence.indexOf("b")).isEqualTo(2);
|
||||
assertThat(sequence.subSequence(2).indexOf("a")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfStringFromIndexShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
|
||||
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
|
||||
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCodeShouldBeSameAsString() {
|
||||
assertThat(new StringSequence("hamburger").hashCode())
|
||||
.isEqualTo("hamburger".hashCode());
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).hashCode())
|
||||
.isEqualTo("urge".hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsWhenSameContentShouldMatch() {
|
||||
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
|
||||
StringSequence b = new StringSequence("urge");
|
||||
StringSequence c = new StringSequence("urgh");
|
||||
assertThat(a).isEqualTo(b).isNotEqualTo(c);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue