nl.topicus.jdbc.shaded.io.netty.util.internal.StringUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spanner-jdbc Show documentation
Show all versions of spanner-jdbc Show documentation
JDBC Driver for Google Cloud Spanner
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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 nl.topicus.jdbc.shaded.io.netty.util.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static nl.topicus.jdbc.shaded.io.netty.util.internal.ObjectUtil.*;
/**
* String utility class.
*/
public final class StringUtil {
public static final String EMPTY_STRING = "";
public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");
public static final char DOUBLE_QUOTE = '\"';
public static final char COMMA = ',';
public static final char LINE_FEED = '\n';
public static final char CARRIAGE_RETURN = '\r';
public static final char TAB = '\t';
public static final char SPACE = 0x20;
private static final String[] BYTE2HEX_PAD = new String[256];
private static final String[] BYTE2HEX_NOPAD = new String[256];
/**
* 2 - Quote character at beginning and end.
* 5 - Extra allowance for anticipated escape characters that may be added.
*/
private static final int CSV_NUMBER_ESCAPE_CHARACTERS = 2 + 5;
private static final char PACKAGE_SEPARATOR_CHAR = '.';
static {
// Generate the lookup table that converts a byte into a 2-digit hexadecimal integer.
int i;
for (i = 0; i < 10; i++) {
BYTE2HEX_PAD[i] = "0" + i;
BYTE2HEX_NOPAD[i] = String.valueOf(i);
}
for (; i < 16; i++) {
char c = (char) ('a' + i - 10);
BYTE2HEX_PAD[i] = "0" + c;
BYTE2HEX_NOPAD[i] = String.valueOf(c);
}
for (; i < BYTE2HEX_PAD.length; i++) {
String str = Integer.toHexString(i);
BYTE2HEX_PAD[i] = str;
BYTE2HEX_NOPAD[i] = str;
}
}
private StringUtil() {
// Unused.
}
/**
* Get the item after one char delim if the delim is found (else null).
* This operation is a simplified and optimized
* version of {@link String#split(String, int)}.
*/
public static String substringAfter(String value, char delim) {
int pos = value.indexOf(delim);
if (pos >= 0) {
return value.substring(pos + 1);
}
return null;
}
/**
* Checks if two strings have the same suffix of specified length
*
* @param s string
* @param p string
* @param len length of the common suffix
* @return true if both s and p are not null and both have the same suffix. Otherwise - false
*/
public static boolean commonSuffixOfLength(String s, String p, int len) {
return s != null && p != null && len >= 0 && s.regionMatches(s.length() - len, p, p.length() - len, len);
}
/**
* Converts the specified byte value into a 2-digit hexadecimal integer.
*/
public static String byteToHexStringPadded(int value) {
return BYTE2HEX_PAD[value & 0xff];
}
/**
* Converts the specified byte value into a 2-digit hexadecimal integer and appends it to the specified buffer.
*/
public static T byteToHexStringPadded(T buf, int value) {
try {
buf.append(byteToHexStringPadded(value));
} catch (IOException e) {
PlatformDependent.throwException(e);
}
return buf;
}
/**
* Converts the specified byte array into a hexadecimal value.
*/
public static String toHexStringPadded(byte[] src) {
return toHexStringPadded(src, 0, src.length);
}
/**
* Converts the specified byte array into a hexadecimal value.
*/
public static String toHexStringPadded(byte[] src, int offset, int length) {
return toHexStringPadded(new StringBuilder(length << 1), src, offset, length).toString();
}
/**
* Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
*/
public static T toHexStringPadded(T dst, byte[] src) {
return toHexStringPadded(dst, src, 0, src.length);
}
/**
* Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
*/
public static T toHexStringPadded(T dst, byte[] src, int offset, int length) {
final int end = offset + length;
for (int i = offset; i < end; i++) {
byteToHexStringPadded(dst, src[i]);
}
return dst;
}
/**
* Converts the specified byte value into a hexadecimal integer.
*/
public static String byteToHexString(int value) {
return BYTE2HEX_NOPAD[value & 0xff];
}
/**
* Converts the specified byte value into a hexadecimal integer and appends it to the specified buffer.
*/
public static T byteToHexString(T buf, int value) {
try {
buf.append(byteToHexString(value));
} catch (IOException e) {
PlatformDependent.throwException(e);
}
return buf;
}
/**
* Converts the specified byte array into a hexadecimal value.
*/
public static String toHexString(byte[] src) {
return toHexString(src, 0, src.length);
}
/**
* Converts the specified byte array into a hexadecimal value.
*/
public static String toHexString(byte[] src, int offset, int length) {
return toHexString(new StringBuilder(length << 1), src, offset, length).toString();
}
/**
* Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
*/
public static T toHexString(T dst, byte[] src) {
return toHexString(dst, src, 0, src.length);
}
/**
* Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
*/
public static T toHexString(T dst, byte[] src, int offset, int length) {
assert length >= 0;
if (length == 0) {
return dst;
}
final int end = offset + length;
final int endMinusOne = end - 1;
int i;
// Skip preceding zeroes.
for (i = offset; i < endMinusOne; i++) {
if (src[i] != 0) {
break;
}
}
byteToHexString(dst, src[i++]);
int remaining = end - i;
toHexStringPadded(dst, src, i, remaining);
return dst;
}
/**
* Helper to decode half of a hexadecimal number from a string.
* @param c The ASCII character of the hexadecimal number to decode.
* Must be in the range {@code [0-9a-fA-F]}.
* @return The hexadecimal value represented in the ASCII character
* given, or {@code -1} if the character is invalid.
*/
public static int decodeHexNibble(final char c) {
// Character.digit() is not used here, as it addresses a larger
// set of characters (both ASCII and full-width latin letters).
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 0xA;
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 0xA;
}
return -1;
}
/**
* Decode a 2-digit hex byte from within a string.
*/
public static byte decodeHexByte(CharSequence s, int pos) {
int hi = decodeHexNibble(s.charAt(pos));
int lo = decodeHexNibble(s.charAt(pos + 1));
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException(String.format(
"invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
}
return (byte) ((hi << 4) + lo);
}
/**
* Decodes part of a string with hex dump
*
* @param hexDump a {@link CharSequence} which contains the hex dump
* @param fromIndex start of hex dump in {@code hexDump}
* @param length hex string length
*/
public static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
if (length < 0 || (length & 1) != 0) {
throw new IllegalArgumentException("length: " + length);
}
if (length == 0) {
return EmptyArrays.EMPTY_BYTES;
}
byte[] bytes = new byte[length >>> 1];
for (int i = 0; i < length; i += 2) {
bytes[i >>> 1] = decodeHexByte(hexDump, fromIndex + i);
}
return bytes;
}
/**
* Decodes a hex dump
*/
public static byte[] decodeHexDump(CharSequence hexDump) {
return decodeHexDump(hexDump, 0, hexDump.length());
}
/**
* The shortcut to {@link #simpleClassName(Class) simpleClassName(o.getClass())}.
*/
public static String simpleClassName(Object o) {
if (o == null) {
return "null_object";
} else {
return simpleClassName(o.getClass());
}
}
/**
* Generates a simplified name from a {@link Class}. Similar to {@link Class#getSimpleName()}, but it works fine
* with anonymous classes.
*/
public static String simpleClassName(Class> clazz) {
String className = checkNotNull(clazz, "clazz").getName();
final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
if (lastDotIdx > -1) {
return className.substring(lastDotIdx + 1);
}
return className;
}
/**
* Escapes the specified value, if necessary according to
* RFC-4180.
*
* @param value The value which will be escaped according to
* RFC-4180
* @return {@link CharSequence} the escaped value if necessary, or the value unchanged
*/
public static CharSequence escapeCsv(CharSequence value) {
return escapeCsv(value, false);
}
/**
* Escapes the specified value, if necessary according to
* RFC-4180.
*
* @param value The value which will be escaped according to
* RFC-4180
* @param trimWhiteSpace The value will first be trimmed of its optional white-space characters,
* according to RFC-7230
* @return {@link CharSequence} the escaped value if necessary, or the value unchanged
*/
public static CharSequence escapeCsv(CharSequence value, boolean trimWhiteSpace) {
int length = checkNotNull(value, "value").length();
int start;
int last;
if (trimWhiteSpace) {
start = indexOfFirstNonOwsChar(value, length);
last = indexOfLastNonOwsChar(value, start, length);
} else {
start = 0;
last = length - 1;
}
if (start > last) {
return EMPTY_STRING;
}
int firstUnescapedSpecial = -1;
boolean quoted = false;
if (isDoubleQuote(value.charAt(start))) {
quoted = isDoubleQuote(value.charAt(last)) && last > start;
if (quoted) {
start++;
last--;
} else {
firstUnescapedSpecial = start;
}
}
if (firstUnescapedSpecial < 0) {
if (quoted) {
for (int i = start; i <= last; i++) {
if (isDoubleQuote(value.charAt(i))) {
if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
firstUnescapedSpecial = i;
break;
}
i++;
}
}
} else {
for (int i = start; i <= last; i++) {
char c = value.charAt(i);
if (c == LINE_FEED || c == CARRIAGE_RETURN || c == COMMA) {
firstUnescapedSpecial = i;
break;
}
if (isDoubleQuote(c)) {
if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
firstUnescapedSpecial = i;
break;
}
i++;
}
}
}
if (firstUnescapedSpecial < 0) {
// Special characters is not found or all of them already escaped.
// In the most cases returns a same string. New string will be instantiated (via StringBuilder)
// only if it really needed. It's important to prevent GC extra load.
return quoted? value.subSequence(start - 1, last + 2) : value.subSequence(start, last + 1);
}
}
StringBuilder result = new StringBuilder(last - start + 1 + CSV_NUMBER_ESCAPE_CHARACTERS);
result.append(DOUBLE_QUOTE).append(value, start, firstUnescapedSpecial);
for (int i = firstUnescapedSpecial; i <= last; i++) {
char c = value.charAt(i);
if (isDoubleQuote(c)) {
result.append(DOUBLE_QUOTE);
if (i < last && isDoubleQuote(value.charAt(i + 1))) {
i++;
}
}
result.append(c);
}
return result.append(DOUBLE_QUOTE);
}
/**
* Unescapes the specified escaped CSV field, if necessary according to
* RFC-4180.
*
* @param value The escaped CSV field which will be unescaped according to
* RFC-4180
* @return {@link CharSequence} the unescaped value if necessary, or the value unchanged
*/
public static CharSequence unescapeCsv(CharSequence value) {
int length = checkNotNull(value, "value").length();
if (length == 0) {
return value;
}
int last = length - 1;
boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1;
if (!quoted) {
validateCsvFormat(value);
return value;
}
StringBuilder unescaped = InternalThreadLocalMap.get().stringBuilder();
for (int i = 1; i < last; i++) {
char current = value.charAt(i);
if (current == DOUBLE_QUOTE) {
if (isDoubleQuote(value.charAt(i + 1)) && (i + 1) != last) {
// Followed by a double-quote but not the last character
// Just skip the next double-quote
i++;
} else {
// Not followed by a double-quote or the following double-quote is the last character
throw newInvalidEscapedCsvFieldException(value, i);
}
}
unescaped.append(current);
}
return unescaped.toString();
}
/**
* Unescapes the specified escaped CSV fields according to
* RFC-4180.
*
* @param value A string with multiple CSV escaped fields which will be unescaped according to
* RFC-4180
* @return {@link List} the list of unescaped fields
*/
public static List unescapeCsvFields(CharSequence value) {
List unescaped = new ArrayList(2);
StringBuilder current = InternalThreadLocalMap.get().stringBuilder();
boolean quoted = false;
int last = value.length() - 1;
for (int i = 0; i <= last; i++) {
char c = value.charAt(i);
if (quoted) {
switch (c) {
case DOUBLE_QUOTE:
if (i == last) {
// Add the last field and return
unescaped.add(current.toString());
return unescaped;
}
char next = value.charAt(++i);
if (next == DOUBLE_QUOTE) {
// 2 double-quotes should be unescaped to one
current.append(DOUBLE_QUOTE);
break;
}
if (next == COMMA) {
// This is the end of a field. Let's start to parse the next field.
quoted = false;
unescaped.add(current.toString());
current.setLength(0);
break;
}
// double-quote followed by other character is invalid
throw newInvalidEscapedCsvFieldException(value, i - 1);
default:
current.append(c);
}
} else {
switch (c) {
case COMMA:
// Start to parse the next field
unescaped.add(current.toString());
current.setLength(0);
break;
case DOUBLE_QUOTE:
if (current.length() == 0) {
quoted = true;
break;
}
// double-quote appears without being enclosed with double-quotes
// fall through
case LINE_FEED:
// fall through
case CARRIAGE_RETURN:
// special characters appears without being enclosed with double-quotes
throw newInvalidEscapedCsvFieldException(value, i);
default:
current.append(c);
}
}
}
if (quoted) {
throw newInvalidEscapedCsvFieldException(value, last);
}
unescaped.add(current.toString());
return unescaped;
}
/**
* Validate if {@code value} is a valid csv field without double-quotes.
*
* @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.
*/
private static void validateCsvFormat(CharSequence value) {
int length = value.length();
for (int i = 0; i < length; i++) {
switch (value.charAt(i)) {
case DOUBLE_QUOTE:
case LINE_FEED:
case CARRIAGE_RETURN:
case COMMA:
// If value contains any special character, it should be enclosed with double-quotes
throw newInvalidEscapedCsvFieldException(value, i);
default:
}
}
}
private static IllegalArgumentException newInvalidEscapedCsvFieldException(CharSequence value, int index) {
return new IllegalArgumentException("invalid escaped CSV field: " + value + " index: " + index);
}
/**
* Get the length of a string, {@code null} input is considered {@code 0} length.
*/
public static int length(String s) {
return s == null ? 0 : s.length();
}
/**
* Determine if a string is {@code null} or {@link String#isEmpty()} returns {@code true}.
*/
public static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
/**
* Find the index of the first non-white space character in {@code s} starting at {@code offset}.
*
* @param seq The string to search.
* @param offset The offset to start searching at.
* @return the index of the first non-white space character or <{@code 0} if none was found.
*/
public static int indexOfNonWhiteSpace(CharSequence seq, int offset) {
for (; offset < seq.length(); ++offset) {
if (!Character.isWhitespace(seq.charAt(offset))) {
return offset;
}
}
return -1;
}
/**
* Determine if {@code c} lies within the range of values defined for
* Surrogate Code Point.
*
* @param c the character to check.
* @return {@code true} if {@code c} lies within the range of values defined for
* Surrogate Code Point. {@code false} otherwise.
*/
public static boolean isSurrogate(char c) {
return c >= '\uD800' && c <= '\uDFFF';
}
private static boolean isDoubleQuote(char c) {
return c == DOUBLE_QUOTE;
}
/**
* Determine if the string {@code s} ends with the char {@code c}.
*
* @param s the string to test
* @param c the tested char
* @return true if {@code s} ends with the char {@code c}
*/
public static boolean endsWith(CharSequence s, char c) {
int len = s.length();
return len > 0 && s.charAt(len - 1) == c;
}
/**
* Trim optional white-space characters from the specified value,
* according to RFC-7230.
*
* @param value the value to trim
* @return {@link CharSequence} the trimmed value if necessary, or the value unchanged
*/
public static CharSequence trimOws(CharSequence value) {
final int length = value.length();
if (length == 0) {
return value;
}
int start = indexOfFirstNonOwsChar(value, length);
int end = indexOfLastNonOwsChar(value, start, length);
return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1);
}
/**
* @return {@code length} if no OWS is found.
*/
private static int indexOfFirstNonOwsChar(CharSequence value, int length) {
int i = 0;
while (i < length && isOws(value.charAt(i))) {
i++;
}
return i;
}
/**
* @return {@code start} if no OWS is found.
*/
private static int indexOfLastNonOwsChar(CharSequence value, int start, int length) {
int i = length - 1;
while (i > start && isOws(value.charAt(i))) {
i--;
}
return i;
}
private static boolean isOws(char c) {
return c == SPACE || c == TAB;
}
}