blob: 2fa8b15d3688353ca33eb45c566ad57536614eca [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.core.util;
import static java.lang.System.lineSeparator;
import static org.eclipse.scout.sdk.core.util.Ensure.newFail;
import java.beans.Introspector;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
/**
* <h3>{@link Strings}</h3> Static utility methods to work with character sequences like {@link String},
* {@link CharSequence}, {@link StringBuilder} or {@code char[]}.
*
* @since 6.1.0
*/
public final class Strings {
private static final int INDEX_NOT_FOUND = -1;
private Strings() {
}
/**
* Checks if the two arrays have the same content comparing the character case sensitive.
*
* <pre>
* first=null & second=null -> true
* first=a & second=a -> true
* first=abc & second=def -> false
* first=null & second=a -> false
* </pre>
*
* @param first
* The first array
* @param second
* The second array
* @return {@code true} if both have equal content or both are {@code null}.
*/
public static boolean equals(char[] first, char[] second) {
//noinspection ArrayEquality
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
if (first.length != second.length) {
return false;
}
for (int i = first.length; --i >= 0;) {
if (first[i] != second[i]) {
return false;
}
}
return true;
}
/**
* Checks if the two arrays have the same content comparing the character using the case sensitivity given. See
* {@link #equals(char[], char[])} for more details.
*
* @param first
* The first array
* @param second
* The second array
* @param isCaseSensitive
* specifies whether or not the equality should be case sensitive
* @return {@code true} if the two arrays are identical character by character according to the value of
* isCaseSensitive or if both are {@code null}.
*/
public static boolean equals(char[] first, char[] second, boolean isCaseSensitive) {
if (isCaseSensitive) {
return equals(first, second);
}
//noinspection ArrayEquality
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
if (first.length != second.length) {
return false;
}
for (int i = first.length; --i >= 0;) {
if (Character.toLowerCase(first[i]) != Character.toLowerCase(second[i])) {
return false;
}
}
return true;
}
/**
* Checks if the two {@link CharSequence}s have the same content comparing the character case sensitive.
*
* <pre>
* first=null & second=null -> true
* first=a & second=a -> true
* first=abc & second=def -> false
* first=null & second=a -> false
* </pre>
*
* @param first
* The first {@link CharSequence}
* @param second
* The second {@link CharSequence}
* @return {@code true} if both have equal content or both are {@code null}.
*/
public static boolean equals(CharSequence first, CharSequence second) {
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
if (first.length() != second.length()) {
return false;
}
for (int i = first.length(); --i >= 0;) {
if (first.charAt(i) != second.charAt(i)) {
return false;
}
}
return true;
}
/**
* Checks if the two {@link CharSequence}s have the same content comparing the character using the case sensitivity
* given. See {@link #equals(CharSequence, CharSequence)} for more details.
*
* @param first
* The first {@link CharSequence}
* @param second
* The second {@link CharSequence}
* @param isCaseSensitive
* specifies whether or not the equality should be case sensitive
* @return {@code true} if the two sequences are identical character by character according to the value of
* isCaseSensitive or if both are {@code null}.
*/
public static boolean equals(CharSequence first, CharSequence second, boolean isCaseSensitive) {
if (isCaseSensitive) {
return equals(first, second);
}
if (first == second) {
return true;
}
if (first == null || second == null) {
return false;
}
if (first.length() != second.length()) {
return false;
}
for (int i = first.length(); --i >= 0;) {
if (Character.toLowerCase(first.charAt(i)) != Character.toLowerCase(second.charAt(i))) {
return false;
}
}
return true;
}
/**
* Gets the first index having the character given.
*
* @param toBeFound
* The character to search
* @param searchIn
* The array to search in. Must not be {@code null}.
* @return The first zero based index having the character given.
* @throws NullPointerException
* if the array is {@code null}.
*/
public static int indexOf(char toBeFound, char[] searchIn) {
return indexOf(toBeFound, searchIn, 0);
}
/**
* Gets the first index having the character given. It starts searching at index start (inclusive) and searches to the
* end of the array.
*
* @param toBeFound
* The character to search
* @param searchIn
* The array to search in. Must not be {@code null}.
* @param start
* The first index to consider.
* @return The first zero based index between start and the end of the array.
* @throws NullPointerException
* if the array is {@code null}.
*/
public static int indexOf(char toBeFound, char[] searchIn, int start) {
return indexOf(toBeFound, searchIn, start, searchIn.length);
}
/**
* Gets the first index having the character given. It starts searching at index start (inclusive) and stops before
* index end (exclusive).
*
* @param toBeFound
* The character to search
* @param searchIn
* The array to search in. Must not be {@code null}.
* @param start
* The first index to consider.
* @param end
* Where to stop searching (exclusive)
* @return The first zero based index between start and end having the character given.
* @throws NullPointerException
* if the array is {@code null}.
*/
public static int indexOf(char toBeFound, char[] searchIn, int start, int end) {
int limit = Math.min(end, searchIn.length);
for (int i = start; i < limit; ++i) {
if (toBeFound == searchIn[i]) {
return i;
}
}
return INDEX_NOT_FOUND;
}
/**
* Gets the first index having the character given.
*
* @param toBeFound
* The character to search
* @param searchIn
* The {@link CharSequence} to search in. Must not be {@code null}.
* @return The first zero based index having the character given.
* @throws NullPointerException
* if the {@link CharSequence} is {@code null}.
*/
public static int indexOf(char toBeFound, CharSequence searchIn) {
return indexOf(toBeFound, searchIn, 0);
}
/**
* Gets the first index having the character given. It starts searching at index start (inclusive) and searches to the
* end of the array.
*
* @param toBeFound
* The character to search
* @param searchIn
* The {@link CharSequence} to search in. Must not be {@code null}.
* @param start
* The first index to consider.
* @return The first zero based index between start and the end of the array.
* @throws NullPointerException
* if the {@link CharSequence} is {@code null}.
*/
public static int indexOf(char toBeFound, CharSequence searchIn, int start) {
return indexOf(toBeFound, searchIn, start, searchIn.length());
}
/**
* Gets the first index having the character given. It starts searching at index start (inclusive) and stops before
* index end (exclusive).
*
* @param toBeFound
* The character to search
* @param searchIn
* The {@link CharSequence} to search in. Must not be {@code null}.
* @param start
* The first index to consider.
* @param end
* Where to stop searching (exclusive)
* @return The first zero based index between start and end having the character given.
* @throws NullPointerException
* if the {@link CharSequence} is {@code null}.
*/
public static int indexOf(char toBeFound, CharSequence searchIn, int start, int end) {
int limit = Math.max(Math.min(end, searchIn.length()), 0);
for (int i = start; i < limit; i++) {
if (toBeFound == searchIn.charAt(i)) {
return i;
}
}
return INDEX_NOT_FOUND;
}
/**
* Like {@link #indexOf(char[], char[])}.
*/
public static int indexOf(CharSequence toBeFound, CharSequence searchIn) {
return indexOf(toBeFound, searchIn, 0);
}
/**
* Like {@link #indexOf(char[], char[], int)}.
*/
public static int indexOf(CharSequence toBeFound, CharSequence searchIn, int start) {
return indexOf(toBeFound, searchIn, start, searchIn.length());
}
/**
* Like {@link #indexOf(char[], char[], int, int)}.
*/
public static int indexOf(CharSequence toBeFound, CharSequence searchIn, int start, int end) {
int toBeFoundLength = toBeFound.length();
if (toBeFoundLength > end || start < 0) {
return INDEX_NOT_FOUND;
}
if (toBeFoundLength == 0) {
return 0;
}
arrayLoop: for (int i = start, max = end - toBeFoundLength + 1; i < max; i++) {
if (searchIn.charAt(i) == toBeFound.charAt(0)) {
for (int j = 1; j < toBeFoundLength; j++) {
if (searchIn.charAt(i + j) != toBeFound.charAt(j)) {
continue arrayLoop;
}
}
return i;
}
}
return INDEX_NOT_FOUND;
}
/**
* Like {@link #indexOf(char[], char[], int, int, boolean)} but performs a case sensitive search in the full array.
*/
public static int indexOf(char[] toBeFound, char[] searchIn) {
return indexOf(toBeFound, searchIn, 0);
}
/**
* Like {@link #indexOf(char[], char[], int, int, boolean)} but performs a case sensitive search from the given start
* (inclusive) to the end of the array.
*/
public static int indexOf(char[] toBeFound, char[] searchIn, int start) {
return indexOf(toBeFound, searchIn, start, searchIn.length);
}
/**
* Like {@link #indexOf(char[], char[], int, int, boolean)} but performs a case sensitive search.
*/
public static int indexOf(char[] toBeFound, char[] searchIn, int start, int end) {
return indexOf(toBeFound, searchIn, start, end, true);
}
/**
* Answers the first index in searchIn for which toBeFound is a matching followup array. Answers -1 if no match is
* found.<br>
* Examples:
* <ol>
* <li>
*
* <pre>
* toBeFound = { 'c' }
* searchIn = { ' a', 'b', 'c', 'd' }
* result => 2
* </pre>
*
* </li>
* <li>
*
* <pre>
* toBeFound = { 'e' }
* searchIn = { ' a', 'b', 'c', 'd' }
* result => -1
* </pre>
*
* </li>
* <li>
*
* <pre>
* toBeFound = { 'b', 'c' }
* searchIn = { ' a', 'b', 'c', 'd' }
* result => 1
* </pre>
*
* </li>
* </ol>
*
* @param toBeFound
* the subarray to search. Must not be {@code null}.
* @param searchIn
* the array to be searched in. Must not be {@code null}.
* @param start
* the starting index (inclusive) describing where in searchIn to begin searching.
* @param end
* the end index (exclusive) describing where in searchIn to stop searching.
* @param isCaseSensitive
* describes if the comparation should be case sensitive or not.
* @return the first index in searchIn for which the toBeFound array is a matching followup array or -1 if it cannot
* be found.
* @throws NullPointerException
* if searchIn is {@code null} or toBeFound is {@code null}
*/
public static int indexOf(char[] toBeFound, char[] searchIn, int start, int end, boolean isCaseSensitive) {
int toBeFoundLength = toBeFound.length;
if (toBeFoundLength > end || start < 0) {
return INDEX_NOT_FOUND;
}
if (toBeFoundLength == 0) {
return 0;
}
if (isCaseSensitive) {
arrayLoop: for (int i = start, max = end - toBeFoundLength + 1; i < max; i++) {
if (searchIn[i] == toBeFound[0]) {
for (int j = 1; j < toBeFoundLength; j++) {
if (searchIn[i + j] != toBeFound[j]) {
continue arrayLoop;
}
}
return i;
}
}
}
else {
arrayLoop: for (int i = start, max = end - toBeFoundLength + 1; i < max; i++) {
if (Character.toLowerCase(searchIn[i]) == Character.toLowerCase(toBeFound[0])) {
for (int j = 1; j < toBeFoundLength; j++) {
if (Character.toLowerCase(searchIn[i + j]) != Character.toLowerCase(toBeFound[j])) {
continue arrayLoop;
}
}
return i;
}
}
}
return INDEX_NOT_FOUND;
}
/**
* Searches for the last index in the {@link CharSequence} which has the character given.
*
* @param toBeFound
* The character to find
* @param searchIn
* The {@link CharSequence} to search in.
* @return The last zero based index or -1 if it could not be found.
* @throws NullPointerException
* if the sequence is {@code null}.
*/
public static int lastIndexOf(char toBeFound, CharSequence searchIn) {
return lastIndexOf(toBeFound, searchIn, 0);
}
/**
* Searches for the last index after the given startIndex which has the character specified.
*
* @param toBeFound
* The character to find.
* @param searchIn
* The {@link CharSequence} to search in.
* @param startIndex
* The index to start.
* @return The last zero based index after the startIndex or -1 if it could not be found.
* @throws NullPointerException
* if the sequence is {@code null}.
*/
public static int lastIndexOf(char toBeFound, CharSequence searchIn, int startIndex) {
return lastIndexOf(toBeFound, searchIn, startIndex, searchIn.length());
}
/**
* Searches for the last index between the startIndex and the endIndex having the given character.
*
* @param toBeFound
* The character to find.
* @param searchIn
* The {@link CharSequence} to search in.
* @param startIndex
* The index where to start the search.
* @param endIndex
* The index where to end the search.
* @returnThe last zero based index between the startIndex and the endIndex or -1 if it could not be found in this
* section.
* @throws NullPointerException
* if the sequence is {@code null}.
*/
@SuppressWarnings("squid:S881")
public static int lastIndexOf(char toBeFound, CharSequence searchIn, int startIndex, int endIndex) {
for (int i = endIndex; --i >= startIndex;) {
if (toBeFound == searchIn.charAt(i)) {
return i;
}
}
return INDEX_NOT_FOUND;
}
/**
* Gets the next index after the given offset at which the current line ends. If invoked for the last line, the array
* length (end) is returned.
*
* @param searchIn
* The array to search in.
* @param offset
* The offset within the array where to start the search.
* @return The next line end character after the given offset. If no one can be found the array length is returned.
*/
@SuppressWarnings("HardcodedLineSeparator")
public static int nextLineEnd(char[] searchIn, int offset) {
int nlPos = indexOf('\n', searchIn, offset);
if (nlPos < 0) {
return searchIn.length; // no more newline found: search to the end of searchIn
}
if (nlPos > 0 && searchIn[nlPos - 1] == '\r') {
nlPos--;
}
return nlPos;
}
/**
* Replaces all occurrences of a character in the specified {@link CharSequence} with another character.
*
* @param text
* The text in which the characters should be replaced.
* @param search
* The character to be replaced.
* @param replacement
* The new character to insert instead.
* @return A {@link CharSequence} with the new content or {@code null} if the input text is {@code null}.
*/
public static CharSequence replace(CharSequence text, char search, char replacement) {
if (text == null) {
return null;
}
if (text.length() < 1) {
return "";
}
StringBuilder result = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == search) {
result.append(replacement);
}
else {
result.append(c);
}
}
return result;
}
/**
* Converts the {@link StringBuilder} specified into a {@code char[]}.
*
* @param s
* The {@link StringBuilder} to convert. Must not be {@code null}.
* @return The full contents as {@code char[]}.
* @throws IllegalArgumentException
* if the {@link StringBuilder} is {@code null}.
*/
public static char[] toCharArray(StringBuilder s) {
Ensure.notNull(s);
char[] buf = new char[s.length()];
s.getChars(0, buf.length, buf, 0);
return buf;
}
/**
* <p>
* Repeat a {@link CharSequence} n times to form a new {@link CharSequence}.
* </p>
* <b>Examples:</b>
*
* <pre>
* repeat(null, 2) = null
* repeat("", 0) = ""
* repeat("", 2) = ""
* repeat("a", 3) = "aaa"
* repeat("ab", 2) = "abab"
* repeat("a", -2) = ""
* </pre>
*
* @param str
* the {@link CharSequence} to repeat, may be {@code null}.
* @param n
* number of times to repeat, negative treated as zero.
* @return a new {@link CharSequence} consisting of the original CharSequence repeated
*/
public static CharSequence repeat(CharSequence str, int n) {
if (str == null) {
return null;
}
if (n < 1 || str.length() < 1) {
return "";
}
StringBuilder b = new StringBuilder(str.length() * n);
for (int i = 0; i < n; i++) {
b.append(str);
}
return b;
}
/**
* <p>
* Replaces a {@link CharSequence} with another {@link CharSequence} inside a larger {@link CharSequence}
* </p>
* <p>
* A {@code null} reference passed to this method is a no-op.
* </p>
* <p>
* The difference of this method to {@link String#replace(CharSequence, CharSequence)} is that this implementation
* does not make use of a regular-expression-pattern and is {@code null} safe.
* </p>
* <b>Examples:</b>
*
* <pre>
* replace(null, *, *) = null
* replace("", *, *) = ""
* replace("any", null, *) = "any"
* replace("any", *, null) = "any"
* replace("any", "", *) = "any"
* replace("abaa", "a", null) = "abaa"
* replace("abaa", "a", "") = "b"
* replace("abaa", "a", "z") = "abaa"
* </pre>
*
* @param text
* text to search and replace in, may be {@code null}.
* @param searchString
* the {@link CharSequence} to search for, may be {@code null}.
* @param replacement
* the {@link CharSequence} to replace it with, may be {@code null}.
* @return the text with any replacements processed, {@code null} if {@code null} input.
*/
public static CharSequence replace(CharSequence text, CharSequence searchString, CharSequence replacement) {
if (isEmpty(text) || isEmpty(searchString) || replacement == null || Objects.equals(searchString, replacement)) {
return text;
}
int start = 0;
int end = indexOf(searchString, text, start);
if (end == INDEX_NOT_FOUND) {
return text;
}
int replLength = searchString.length();
int increase = replacement.length() - replLength;
increase = Math.max(increase, 0);
increase *= 16;
StringBuilder buf = new StringBuilder(text.length() + increase);
while (end != INDEX_NOT_FOUND) {
buf.append(text, start, end).append(replacement);
start = end + replLength;
end = indexOf(searchString, text, start);
}
buf.append(text.subSequence(start, text.length()));
return buf;
}
/**
* <p>
* Counts how many times the substring appears in the larger {@link CharSequence}.
* </p>
* <p>
* A {@code null} or empty ("") {@link CharSequence} input returns {@code 0}.
* </p>
* <p>
* <p>
* <p>
*
* <pre>
* countMatches(null, *) = 0
* countMatches("", *) = 0
* countMatches("abba", null) = 0
* countMatches("abba", "") = 0
* countMatches("abba", "a") = 2
* countMatches("abba", "ab") = 1
* countMatches("abba", "xxx") = 0
* </pre>
*
* @param str
* the {@link CharSequence} to check, may be {@code null}.
* @param sub
* the substring to count, may be {@code null}.
* @return the number of occurrences, 0 if either {@link CharSequence} is {@code null}.
*/
public static int countMatches(CharSequence str, CharSequence sub) {
if (isEmpty(str) || isEmpty(sub)) {
return 0;
}
int count = 0;
int idx = 0;
while ((idx = indexOf(sub, str, idx)) != INDEX_NOT_FOUND) {
count++;
idx += sub.length();
}
return count;
}
/**
* <p>
* Checks if a CharSequence is empty ("") or {@code null}.
* </p>
* <p>
* <p>
* <p>
*
* <pre>
* Strings.isEmpty(null) = true
* Strings.isEmpty("") = true
* Strings.isEmpty(" ") = false
* Strings.isEmpty("bob") = false
* Strings.isEmpty(" bob ") = false
* </pre>
*
* @param cs
* the CharSequence to check, may be {@code null}
* @return {@code true} if the CharSequence is empty or {@code null}
*/
public static boolean isEmpty(CharSequence cs) {
return cs == null || cs.length() == 0;
}
/**
* Reads all bytes from the given {@link InputStream} and converts them into a {@link StringBuilder} using the given
* charset name.<br>
*
* @param is
* The data source. Must not be {@code null}.
* @param charsetName
* The name of the {@link Charset} to use. Must be supported by the platform.
* @return A {@link StringBuilder} holding the contents.
* @throws IOException
* While reading data from the stream or if the given charsetName does not exist on this platform.
* @see Charset#isSupported(String)
*/
public static StringBuilder fromInputStream(InputStream is, String charsetName) throws IOException {
if (!Charset.isSupported(charsetName)) {
throw new IOException("Charset '" + charsetName + "' is not supported.");
}
return fromInputStream(is, Charset.forName(charsetName));
}
/**
* Creates a {@link String} holding the content of the file specified.
*
* @param file
* The file to load. Must not be {@code null}.
* @param charset
* The {@link Charset} to use to transform the bytes in the file into characters. Consider using one of the
* {@link StandardCharsets} constants. Must not be {@code null}.
* @return A {@link String} holding the content.
* @throws IOException
* If {@link Path} does not point to a readable file or there was an error during read.
*/
public static String fromFileAsString(Path file, Charset charset) throws IOException {
Ensure.notNull(charset);
return new String(fileRawBytes(file), charset);
}
/**
* Creates a char array holding the content of the file specified.
*
* @param file
* The file to load. Must not be {@code null}.
* @param charset
* The {@link Charset} to use to transform the bytes in the file into characters. Consider using one of the
* {@link StandardCharsets} constants. Must not be {@code null}.
* @return The chars of the file.
* @throws IOException
* If {@link Path} does not point to a readable file or there was an error during read.
*/
public static char[] fromFileAsChars(Path file, Charset charset) throws IOException {
Ensure.notNull(charset);
return charset.decode(ByteBuffer.wrap(fileRawBytes(file))).array();
}
/**
* Creates a {@link CharSequence} holding the content of the file specified.
*
* @param file
* The file to load. Must not be {@code null}.
* @param charset
* The {@link Charset} to use to transform the bytes in the file into characters. Consider using one of the
* {@link StandardCharsets} constants. Must not be {@code null}.
* @return A {@link CharSequence} holding the content.
* @throws IOException
* If {@link Path} does not point to a readable file or there was an error during read.
*/
public static CharSequence fromFileAsCharSequence(Path file, Charset charset) throws IOException {
return CharBuffer.wrap(fromFileAsChars(file, charset));
}
private static byte[] fileRawBytes(Path file) throws IOException {
Ensure.notNull(file);
if (!Files.isRegularFile(file) || !Files.isReadable(file)) {
throw new IOException(file + " cannot be read.");
}
return Files.readAllBytes(file);
}
/**
* Reads all bytes from the given {@link InputStream} and converts them into a {@link StringBuilder} using the given
* {@link Charset}.<br>
* The specified {@link InputStream} is not closed!
*
* @param is
* The data source. Must not be {@code null}.
* @param charset
* The {@link Charset} to use for the byte-to-char conversion.
* @return A {@link StringBuilder} holding the contents.
* @throws IOException
* While reading data from the stream.
*/
public static StringBuilder fromInputStream(InputStream is, Charset charset) throws IOException {
char[] buffer = new char[8192];
StringBuilder out = new StringBuilder(buffer.length);
int length;
//noinspection resource,IOResourceOpenedButNotSafelyClosed
Reader in = new InputStreamReader(is, charset);
while ((length = in.read(buffer)) != INDEX_NOT_FOUND) {
out.append(buffer, 0, length);
}
return out;
}
/**
* Converts the stack trace of the given {@link Throwable} into a {@link String}.
* <p>
* The resulting {@link String} contains no leading or trailing line separators.
*
* @param t
* The {@link Throwable}. Must not be {@code null}.
* @return The {@link String} describing the given {@link Throwable}.
*/
@SuppressWarnings({"squid:S1148", "squid:S1166"})
public static String fromThrowable(Throwable t) {
try (StringWriter w = new StringWriter(); PrintWriter p = new PrintWriter(w)) {
t.printStackTrace(p);
StringBuffer buffer = w.getBuffer();
buffer.delete(buffer.length() - lineSeparator().length(), buffer.length());
return w.toString();
}
catch (IOException e) {
return '[' + e.toString() + ']' + t;
}
}
/**
* Converts the given input string literal into the representing original string.<br>
* This is the inverse function of {@link #toStringLiteral(CharSequence)}.
*
* @param s
* The literal with leading and ending double-quotes
* @return the original (un-escaped) string. if it is no valid literal string, {@code null} is returned.
*/
public static CharSequence fromStringLiteral(CharSequence s) {
if (s == null) {
return null;
}
return replaceLiterals(withoutQuotes(s), true);
}
private static CharSequence replaceLiterals(CharSequence result, boolean fromLiteral) {
//noinspection HardcodedLineSeparator
CharSequence[] a = {"\b", "\t", "\n", "\f", "\r", "\"", "\\", "\0", "\1", "\2", "\3", "\4", "\5", "\6", "\7"};
//noinspection HardcodedLineSeparator
CharSequence[] b = {"\\b", "\\t", "\\n", "\\f", "\\r", "\\\"", "\\\\", "\\0", "\\1", "\\2", "\\3", "\\4", "\\5", "\\6", "\\7"};
if (fromLiteral) {
return replaceEach(result, b, a);
}
return replaceEach(result, a, b);
}
/**
* Converts the given {@link CharSequence} into a string literal with leading and ending double-quotes including
* escaping of the given string.<br>
* This is the inverse function of {@link #fromStringLiteral(CharSequence)}.
*
* @param s
* the string to convert.
* @return the literal string ready to be directly inserted into java source or null if the input string is null.
*/
public static CharSequence toStringLiteral(CharSequence s) {
if (s == null) {
return null;
}
StringBuilder b = new StringBuilder(s.length() * 2);
b.append('"'); // opening delimiter
b.append(replaceLiterals(s, false));
b.append('"'); // closing delimiter
return b;
}
/**
* Removes leading or trailing double quotes ("), single quotes (') or back ticks (`) from the input.
*
* @param literal
* The literal from which the quotes should be removed.
* @return The input with removed quotes.
*/
public static CharSequence withoutQuotes(CharSequence literal) {
return withoutQuotes(literal, true, true, true);
}
/**
* Removes leading and trailing quotes (if existing) from the literal given.<br>
* Only the first quotes are removed. If there are nested quotes, the are part of the result.
*
* @param literal
* The literal from which the quotes should be removed.
* @param removeDouble
* {@code true} if double quotes (") should be removed if found.
* @param removeSingle
* {@code true} if single quotes (') should be removed if found.
* @param removeBackTick
* {@code true} if back ticks (`) should be removed if found.
* @return The input with removed leading and trailing quotes respecting the enabled quote types.
*/
public static CharSequence withoutQuotes(CharSequence literal, boolean removeDouble, boolean removeSingle, boolean removeBackTick) {
if (literal == null || literal.length() < 2 || (!removeDouble && !removeSingle && !removeBackTick)) {
return literal;
}
boolean[] enabled = new boolean[]{removeDouble, removeSingle, removeBackTick};
char[] toRemove = new char[]{'"', '\'', '`'};
for (int i = 0; i < toRemove.length; i++) {
if (enabled[i] && literal.charAt(0) == toRemove[i] && literal.charAt(literal.length() - 1) == toRemove[i]) {
return literal.subSequence(1, literal.length() - 1);
}
}
return literal;
}
/**
* Ensures the given name starts with an upper case character.<br>
* <br>
* <b>Note:</b><br>
* To ensure the first char starts with a lower case letter use {@link Introspector#decapitalize(String)}
*
* @param name
* The name to handle.
* @return null if the input is null, an empty string if the given string is empty or only contains white spaces.
* Otherwise the input string is returned with the first character modified to upper case.
*/
public static CharSequence ensureStartWithUpperCase(CharSequence name) {
if (isEmpty(name) || Character.isUpperCase(name.charAt(0))) {
return name;
}
return new StringBuilder(name.length())
.append(Character.toUpperCase(name.charAt(0)))
.append(name, 1, name.length());
}
/**
* Returns the given input HTML with all necessary characters escaped.
*
* @param html
* The input HTML.
* @return The escaped version.
*/
public static CharSequence escapeHtml(CharSequence html) {
return replaceEach(html,
new CharSequence[]{"\"", "&", "<", ">", "'", "/"},
new CharSequence[]{"&#92;", "&#38;", "&#60;", "&#62;", "&#39;", "&#47;"});
}
/**
* <p>
* Replaces all occurrences of strings within another string.
* </p>
* <p>
* A {@code null} reference passed to this method is a no-op, or if any "search string" or "string to replace" is
* {@code null}, that replace will be ignored.
* </p>
* <p>
* <p>
* <p>
*
* <pre>
* replaceEach(null, *, *) = null
* replaceEach("", *, *) = ""
* replaceEach("aba", null, null) = "aba"
* replaceEach("aba", new String[0], null) = "aba"
* replaceEach("aba", null, new String[0]) = "aba"
* replaceEach("aba", new String[]{"a"}, null) = "aba"
* replaceEach("aba", new String[]{"a"}, new String[]{""}) = "b"
* replaceEach("aba", new String[]{null}, new String[]{"a"}) = "aba"
* replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}) = "wcte"
* </pre>
*
* @param text
* text to search and replace in, no-op if {@code null}.
* @param searchList
* the strings to search for, no-op if {@code null}.
* @param replacementList
* the strings to replace them with, no-op if {@code null}.
* @return the text with any replacements processed, {@code null} if {@code null} input.
* @throws IllegalArgumentException
* if the lengths of the arrays are not the same ({@code null} is ok, and/or size 0)
*/
@SuppressWarnings("pmd:NPathComplexity")
public static CharSequence replaceEach(CharSequence text, CharSequence[] searchList, CharSequence[] replacementList) {
if (text == null || text.length() == 0) {
return text;
}
if (searchList == null || searchList.length == 0) {
return text;
}
if (replacementList == null || replacementList.length == 0) {
return text;
}
int searchLength = searchList.length;
int replacementLength = replacementList.length;
if (searchLength != replacementLength) { // make sure lengths are ok, these need to be equal
throw newFail("Search and Replace array lengths don't match: {} vs {}", searchLength, replacementLength);
}
// keep track of which still have matches
boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];
// index on index that the match was found
int textIndex = INDEX_NOT_FOUND;
int replaceIndex = INDEX_NOT_FOUND;
int tempIndex;
// index of replace array that will replace the search string found
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || searchList[i] == null || searchList[i].length() == 0 || replacementList[i] == null) {
continue;
}
tempIndex = indexOf(searchList[i], text);
// see if we need to keep searching for this
if (tempIndex == INDEX_NOT_FOUND) {
noMoreMatchesForReplIndex[i] = true;
}
else {
if (textIndex == INDEX_NOT_FOUND || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
}
// no search strings found, we are done
if (textIndex == INDEX_NOT_FOUND) {
return text;
}
int start = 0;
// get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit
int increase = getLengthIncreaseGuess(text, searchList, replacementList);
StringBuilder result = new StringBuilder(text.length() + increase);
while (textIndex != INDEX_NOT_FOUND) {
for (int i = start; i < textIndex; i++) {
result.append(text.charAt(i));
}
result.append(replacementList[replaceIndex]);
start = textIndex + searchList[replaceIndex].length();
textIndex = INDEX_NOT_FOUND;
replaceIndex = INDEX_NOT_FOUND;
// find the next earliest match
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || searchList[i] == null || searchList[i].length() == 0 || replacementList[i] == null) {
continue;
}
tempIndex = indexOf(searchList[i], text, start);
// see if we need to keep searching for this
if (tempIndex == INDEX_NOT_FOUND) {
noMoreMatchesForReplIndex[i] = true;
}
else {
if (textIndex == INDEX_NOT_FOUND || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
}
}
int textLength = text.length();
for (int i = start; i < textLength; i++) {
result.append(text.charAt(i));
}
return result;
}
private static int getLengthIncreaseGuess(CharSequence text, CharSequence[] searchList, CharSequence[] replacementList) {
int increase = 0;
// count the replacement text elements that are larger than their corresponding text being replaced
for (int i = 0; i < searchList.length; i++) {
if (searchList[i] == null || replacementList[i] == null) {
continue;
}
int longer = replacementList[i].length() - searchList[i].length();
if (longer > 0) {
increase += 3 * longer; // assume 3 matches
}
}
// have upper-bound at 20% increase, then let Java take over
return Math.min(increase, text.length() / 5);
}
/**
* Checks if a {@link CharSequence} contains only invisible characters.
* <p>
*
* <pre>
* isBlank(null) = true
* isBlank("") = true
* isBlank(" ") = true
* isBlank("bob") = false
* isBlank(" bob ") = false
* </pre>
*
* @param cs
* the CharSequence to check, may be null
* @return {@code true} if the CharSequence is null, empty or whitespace
* @see #hasText(CharSequence)
*/
public static boolean isBlank(CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
/**
* Tests if the string specified ends with the specified suffix.
*
* @param string
* The {@link CharSequence} to test
* @param suffix
* The suffix to search
* @return {@code true} if the string ends with the specified suffix or the suffix has length 0. If the string or
* suffix are {@code null} or the string does not end with the suffix specified, {@code false} is returned.
*/
public static boolean endsWith(CharSequence string, CharSequence suffix) {
if (string == null || suffix == null) {
return false;
}
if (suffix.length() == 0) {
return true;
}
if (string.length() < suffix.length()) {
return false;
}
for (int i = string.length() - 1, j = suffix.length() - 1; j >= 0; i--, j--) {
if (string.charAt(i) != suffix.charAt(j)) {
return false;
}
}
return true;
}
/**
* Checks if a {@link CharSequence} contains visible characters.
* <p>
*
* <pre>
* hasText(null) = false
* hasText("") = false
* hasText(" ") = false
* hasText("bob") = true
* hasText(" bob ") = true
* </pre>
*
* @param cs
* the CharSequence to check, may be null
* @return {@code true} if the CharSequence is null, empty or whitespace
* @see #isBlank(CharSequence)
*/
public static boolean hasText(CharSequence cs) {
return !isBlank(cs);
}
/**
* Wraps a {@link CharSequence} into an {@link Optional} holding a value if the given sequence is not empty.
*
* @param value
* The {@link CharSequence} to wrap
* @return If the given {@link CharSequence} is neither {@code null} nor of length zero, an {@link Optional} holding
* the value. Otherwise an empty {@link Optional} is returned.
* @see #isEmpty(CharSequence)
*/
public static <T extends CharSequence> Optional<T> notEmpty(T value) {
if (isEmpty(value)) {
return Optional.empty();
}
return Optional.of(value);
}
/**
* Wraps a {@link CharSequence} into an {@link Optional} holding a value if the given sequence is not blank.
*
* @param value
* The {@link CharSequence} to wrap
* @return An {@link Optional} holding the value if the given {@link CharSequence} contains visible characters.
* Otherwise an empty {@link Optional} is returned.
* @see #isBlank(CharSequence)
*/
public static <T extends CharSequence> Optional<T> notBlank(T value) {
if (isBlank(value)) {
return Optional.empty();
}
return Optional.of(value);
}
/**
* Compares two {@link CharSequence}s lexicographically.
*
* @param a
* The first {@link CharSequence}. Must not be {@code null}.
* @param b
* The second {@link CharSequence}. Must not be {@code null}.
* @return the value {@code 0} if the two {@link CharSequence}s are equal on every character they contain. A value
* less than {@code 0} if the first {@link CharSequence} is lexicographically less than the second. A value
* greater than {@code 0} if the first {@link CharSequence} is lexicographically greater than the second.
* @see String#compareTo(String)
*/
public static int compareTo(CharSequence a, CharSequence b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return INDEX_NOT_FOUND;
}
if (b == null) {
return 1;
}
int limit = Math.min(a.length(), b.length());
for (int i = 0; i < limit; i++) {
char x = a.charAt(i);
char y = b.charAt(i);
//noinspection CharUsedInArithmeticContext
int diff = x - y;
if (diff != 0) {
return diff;
}
}
return a.length() - b.length();
}
}