Commit aa66d5df authored by Phillip Webb's avatar Phillip Webb

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
parent c0243131
...@@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets; ...@@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets;
*/ */
final class AsciiBytes { final class AsciiBytes {
private static final int[] EXCESS = { 0x0, 0x1080, 0x96, 0x1c82080 };
private final byte[] bytes; private final byte[] bytes;
private final int offset; private final int offset;
...@@ -118,36 +120,56 @@ final class AsciiBytes { ...@@ -118,36 +120,56 @@ final class AsciiBytes {
return new AsciiBytes(this.bytes, this.offset + beginIndex, length); return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
} }
public AsciiBytes append(String string) { @Override
if (string == null || string.isEmpty()) { public String toString() {
return this; if (this.string == null) {
this.string = new String(this.bytes, this.offset, this.length,
StandardCharsets.UTF_8);
} }
return append(string.getBytes(StandardCharsets.UTF_8)); return this.string;
} }
public AsciiBytes append(AsciiBytes asciiBytes) { public boolean matches(CharSequence name, char suffix) {
if (asciiBytes == null || asciiBytes.length() == 0) { int charIndex = 0;
return this; 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 append(asciiBytes.bytes); return charIndex == totalLen;
} }
public AsciiBytes append(byte[] bytes) { private char getChar(CharSequence name, char suffix, int index) {
if (bytes == null || bytes.length == 0) { if (index < name.length()) {
return this; return name.charAt(index);
} }
byte[] combined = new byte[this.length + bytes.length]; if (index == name.length()) {
System.arraycopy(this.bytes, this.offset, combined, 0, this.length); return suffix;
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);
} }
return this.string; return 0;
} }
@Override @Override
...@@ -158,24 +180,11 @@ final class AsciiBytes { ...@@ -158,24 +180,11 @@ final class AsciiBytes {
int b = this.bytes[i]; int b = this.bytes[i];
if (b < 0) { if (b < 0) {
b = b & 0x7F; b = b & 0x7F;
int limit; int limit = getRemainingUtfBytes(b);
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);
}
for (int j = 0; j < limit; j++) { for (int j = 0; j < limit; j++) {
b = (b << 6) + (this.bytes[++i] & 0xFF); b = (b << 6) + (this.bytes[++i] & 0xFF);
} }
b -= excess; b -= EXCESS[limit];
} }
if (b <= 0xFFFF) { if (b <= 0xFFFF) {
hash = 31 * hash + b; hash = 31 * hash + b;
...@@ -190,6 +199,10 @@ final class AsciiBytes { ...@@ -190,6 +199,10 @@ final class AsciiBytes {
return hash; return hash;
} }
private int getRemainingUtfBytes(int b) {
return (b < 96 ? 1 : (b < 112 ? 2 : 3));
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj == null) { if (obj == null) {
...@@ -216,16 +229,17 @@ final class AsciiBytes { ...@@ -216,16 +229,17 @@ final class AsciiBytes {
return new String(bytes, StandardCharsets.UTF_8); return new String(bytes, StandardCharsets.UTF_8);
} }
public static int hashCode(String string) { public static int hashCode(CharSequence charSequence) {
// We're compatible with String's hashCode(). // We're compatible with String's hashCode()
return string.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) { public static int hashCode(int hash, char suffix) {
for (int i = 0; i < string.length(); i++) { return (suffix == 0 ? hash : (31 * hash + suffix));
hash = 31 * hash + string.charAt(i);
}
return hash;
} }
} }
...@@ -101,8 +101,8 @@ final class CentralDirectoryFileHeader implements FileHeader { ...@@ -101,8 +101,8 @@ final class CentralDirectoryFileHeader implements FileHeader {
} }
@Override @Override
public boolean hasName(String name, String suffix) { public boolean hasName(CharSequence name, char suffix) {
return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix)); return this.name.matches(name, suffix);
} }
public boolean isDirectory() { public boolean isDirectory() {
......
...@@ -30,10 +30,10 @@ interface FileHeader { ...@@ -30,10 +30,10 @@ interface FileHeader {
/** /**
* Returns {@code true} if the header has the given name. * Returns {@code true} if the header has the given name.
* @param name the name to test * @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 * @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. * Return the offset of the load file header within the archive data.
......
...@@ -31,6 +31,8 @@ import java.util.jar.Manifest; ...@@ -31,6 +31,8 @@ import java.util.jar.Manifest;
*/ */
class JarEntry extends java.util.jar.JarEntry implements FileHeader { class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private final AsciiBytes name;
private Certificate[] certificates; private Certificate[] certificates;
private CodeSigner[] codeSigners; private CodeSigner[] codeSigners;
...@@ -41,6 +43,7 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { ...@@ -41,6 +43,7 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) { JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
super(header.getName().toString()); super(header.getName().toString());
this.name = header.getName();
this.jarFile = jarFile; this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset(); this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize()); setCompressedSize(header.getCompressedSize());
...@@ -53,10 +56,13 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { ...@@ -53,10 +56,13 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
setTime(header.getTime()); setTime(header.getTime());
} }
AsciiBytes getAsciiBytesName() {
return this.name;
}
@Override @Override
public boolean hasName(String name, String suffix) { public boolean hasName(CharSequence name, char suffix) {
return getName().length() == name.length() + suffix.length() return this.name.matches(name, suffix);
&& getName().startsWith(name) && getName().endsWith(suffix);
} }
/** /**
......
...@@ -191,6 +191,10 @@ public class JarFile extends java.util.jar.JarFile { ...@@ -191,6 +191,10 @@ public class JarFile extends java.util.jar.JarFile {
}; };
} }
public JarEntry getJarEntry(CharSequence name) {
return this.entries.getEntry(name);
}
@Override @Override
public JarEntry getJarEntry(String name) { public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name); return (JarEntry) getEntry(name);
...@@ -228,8 +232,7 @@ public class JarFile extends java.util.jar.JarFile { ...@@ -228,8 +232,7 @@ public class JarFile extends java.util.jar.JarFile {
* @return a {@link JarFile} for the entry * @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read * @throws IOException if the nested jar file cannot be read
*/ */
public synchronized JarFile getNestedJarFile(ZipEntry entry) public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
throws IOException {
return getNestedJarFile((JarEntry) entry); return getNestedJarFile((JarEntry) entry);
} }
...@@ -257,16 +260,16 @@ public class JarFile extends java.util.jar.JarFile { ...@@ -257,16 +260,16 @@ public class JarFile extends java.util.jar.JarFile {
} }
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
final AsciiBytes sourceName = new AsciiBytes(entry.getName()); AsciiBytes name = entry.getAsciiBytesName();
JarEntryFilter filter = (name) -> { JarEntryFilter filter = (candidate) -> {
if (name.startsWith(sourceName) && !name.equals(sourceName)) { if (candidate.startsWith(name) && !candidate.equals(name)) {
return name.substring(sourceName.length()); return candidate.substring(name.length());
} }
return null; return null;
}; };
return new JarFile(this.rootFile, return new JarFile(this.rootFile,
this.pathFromRoot + "!/" this.pathFromRoot + "!/"
+ entry.getName().substring(0, sourceName.length() - 1), + entry.getName().substring(0, name.length() - 1),
this.data, filter, JarFileType.NESTED_DIRECTORY); this.data, filter, JarFileType.NESTED_DIRECTORY);
} }
......
...@@ -46,9 +46,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -46,9 +46,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private static final long LOCAL_FILE_HEADER_SIZE = 30; 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; protected static final int ENTRY_CACHE_SIZE = 25;
...@@ -166,11 +166,11 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -166,11 +166,11 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return new EntryIterator(); return new EntryIterator();
} }
public boolean containsEntry(String name) { public boolean containsEntry(CharSequence name) {
return getEntry(name, FileHeader.class, true) != null; return getEntry(name, FileHeader.class, true) != null;
} }
public JarEntry getEntry(String name) { public JarEntry getEntry(CharSequence name) {
return getEntry(name, JarEntry.class, true); return getEntry(name, JarEntry.class, true);
} }
...@@ -213,7 +213,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -213,7 +213,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
+ nameLength + extraLength, entry.getCompressedSize()); + 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) { boolean cacheEntry) {
int hashCode = AsciiBytes.hashCode(name); int hashCode = AsciiBytes.hashCode(name);
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry); T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
...@@ -224,8 +224,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> { ...@@ -224,8 +224,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return entry; return entry;
} }
private <T extends FileHeader> T getEntry(int hashCode, String name, String suffix, private <T extends FileHeader> T getEntry(int hashCode, CharSequence name,
Class<T> type, boolean cacheEntry) { char suffix, Class<T> type, boolean cacheEntry) {
int index = getFirstIndex(hashCode); int index = getFirstIndex(hashCode);
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
T entry = getEntry(index, type, cacheEntry); T entry = getEntry(index, type, cacheEntry);
......
...@@ -68,7 +68,8 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -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"; private static final String READ_ACTION = "read";
...@@ -254,17 +255,17 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -254,17 +255,17 @@ final class JarURLConnection extends java.net.JarURLConnection {
} }
static JarURLConnection get(URL url, JarFile jarFile) throws IOException { 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 separator;
int index = 0;
while ((separator = spec.indexOf(SEPARATOR, 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); JarEntry jarEntry = jarFile.getJarEntry(entryName);
if (jarEntry == null) { if (jarEntry == null) {
return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName)); return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName));
} }
jarFile = jarFile.getNestedJarFile(jarEntry); jarFile = jarFile.getNestedJarFile(jarEntry);
index += separator + SEPARATOR.length(); index = separator + SEPARATOR.length();
} }
JarEntryName jarEntryName = JarEntryName.get(spec, index); JarEntryName jarEntryName = JarEntryName.get(spec, index);
if (Boolean.TRUE.equals(useFastExceptions.get())) { if (Boolean.TRUE.equals(useFastExceptions.get())) {
...@@ -276,14 +277,12 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -276,14 +277,12 @@ final class JarURLConnection extends java.net.JarURLConnection {
return new JarURLConnection(url, jarFile, jarEntryName); return new JarURLConnection(url, jarFile, jarEntryName);
} }
private static String extractFullSpec(URL url, String pathFromRoot) { private static int indexOfRootSpec(StringSequence file, String pathFromRoot) {
String file = url.getFile();
int separatorIndex = file.indexOf(SEPARATOR); int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0) { if (separatorIndex < 0) {
return ""; return -1;
} }
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length(); return separatorIndex + SEPARATOR.length() + pathFromRoot.length();
return file.substring(specIndex);
} }
private static JarURLConnection notFound() { private static JarURLConnection notFound() {
...@@ -308,22 +307,22 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -308,22 +307,22 @@ final class JarURLConnection extends java.net.JarURLConnection {
*/ */
static class JarEntryName { static class JarEntryName {
private final String name; private final StringSequence name;
private String contentType; private String contentType;
JarEntryName(String spec) { JarEntryName(StringSequence spec) {
this.name = decode(spec); this.name = decode(spec);
} }
private String decode(String source) { private StringSequence decode(StringSequence source) {
if (source.isEmpty() || (source.indexOf('%') < 0)) { if (source.isEmpty() || (source.indexOf('%') < 0)) {
return source; return source;
} }
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); 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 // 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) { private void write(String source, ByteArrayOutputStream outputStream) {
...@@ -367,7 +366,7 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -367,7 +366,7 @@ final class JarURLConnection extends java.net.JarURLConnection {
@Override @Override
public String toString() { public String toString() {
return this.name; return this.name.toString();
} }
public boolean isEmpty() { public boolean isEmpty() {
...@@ -389,15 +388,15 @@ final class JarURLConnection extends java.net.JarURLConnection { ...@@ -389,15 +388,15 @@ final class JarURLConnection extends java.net.JarURLConnection {
return type; return type;
} }
public static JarEntryName get(String spec) { public static JarEntryName get(StringSequence spec) {
return get(spec, 0); return get(spec, 0);
} }
public static JarEntryName get(String spec, int beginIndex) { public static JarEntryName get(StringSequence spec, int beginIndex) {
if (spec.length() <= beginIndex) { if (spec.length() <= beginIndex) {
return EMPTY_JAR_ENTRY_NAME; return EMPTY_JAR_ENTRY_NAME;
} }
return new JarEntryName(spec.substring(beginIndex)); return new JarEntryName(spec.subSequence(beginIndex));
} }
} }
......
/*
* 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; ...@@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class AsciiBytesTests { public class AsciiBytesTests {
private static final char NO_SUFFIX = 0;
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
...@@ -106,22 +108,6 @@ public class AsciiBytesTests { ...@@ -106,22 +108,6 @@ public class AsciiBytesTests {
abcd.substring(3, 5); 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 @Test
public void hashCodeAndEquals() { public void hashCodeAndEquals() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
...@@ -163,4 +149,42 @@ public class AsciiBytesTests { ...@@ -163,4 +149,42 @@ public class AsciiBytesTests {
assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode()); 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();
}
} }
/*
* 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; ...@@ -26,6 +26,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -155,6 +156,31 @@ public class JarURLConnectionTests { ...@@ -155,6 +156,31 @@ public class JarURLConnectionTests {
.isEqualTo(connection.getJarEntry().getTime()); .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() { private String getAbsolutePath() {
return this.rootJarFile.getAbsolutePath().replace('\\', '/'); return this.rootJarFile.getAbsolutePath().replace('\\', '/');
} }
......
/*
* 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment