/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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 org.apache.camel.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;
import static org.apache.camel.util.StringQuoteHelper.doubleQuote;
/**
* Helper methods for working with Strings.
*/
public final class StringHelper {
/**
* Constructor of utility class should be private.
*/
private StringHelper() {
}
/**
* Ensures that <code>s</code> is friendly for a URL or file system.
*
* @param s String to be sanitized.
* @return sanitized version of <code>s</code>.
* @throws NullPointerException if <code>s</code> is <code>null</code>.
*/
public static String sanitize(String s) {
return s
.replace(':', '-')
.replace('_', '-')
.replace('.', '-')
.replace('/', '-')
.replace('\\', '-');
}
/**
* Counts the number of times the given char is in the string
*
* @param s the string
* @param ch the char
* @return number of times char is located in the string
*/
public static int countChar(String s, char ch) {
if (ObjectHelper.isEmpty(s)) {
return 0;
}
int matches = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (ch == c) {
matches++;
}
}
return matches;
}
/**
* Limits the length of a string
*
* @param s the string
* @param maxLength the maximum length of the returned string
* @return s if the length of s is less than maxLength or the first maxLength characters of s
* @deprecated use {@link #limitLength(String, int)}
*/
@Deprecated
public static String limitLenght(String s, int maxLength) {
return limitLength(s, maxLength);
}
/**
* Limits the length of a string
*
* @param s the string
* @param maxLength the maximum length of the returned string
* @return s if the length of s is less than maxLength or the first maxLength characters of s
*/
public static String limitLength(String s, int maxLength) {
if (ObjectHelper.isEmpty(s)) {
return s;
}
return s.length() <= maxLength ? s : s.substring(0, maxLength);
}
/**
* Removes all quotes (single and double) from the string
*
* @param s the string
* @return the string without quotes (single and double)
*/
public static String removeQuotes(String s) {
if (ObjectHelper.isEmpty(s)) {
return s;
}
s = replaceAll(s, "'", "");
s = replaceAll(s, "\"", "");
return s;
}
/**
* Removes all leading and ending quotes (single and double) from the string
*
* @param s the string
* @return the string without leading and ending quotes (single and double)
*/
public static String removeLeadingAndEndingQuotes(String s) {
if (ObjectHelper.isEmpty(s)) {
return s;
}
String copy = s.trim();
if (copy.startsWith("'") && copy.endsWith("'")) {
return copy.substring(1, copy.length() - 1);
}
if (copy.startsWith("\"") && copy.endsWith("\"")) {
return copy.substring(1, copy.length() - 1);
}
// no quotes, so return as-is
return s;
}
/**
* Whether the string starts and ends with either single or double quotes.
*
* @param s the string
* @return <tt>true</tt> if the string starts and ends with either single or double quotes.
*/
public static boolean isQuoted(String s) {
if (ObjectHelper.isEmpty(s)) {
return false;
}
if (s.startsWith("'") && s.endsWith("'")) {
return true;
}
if (s.startsWith("\"") && s.endsWith("\"")) {
return true;
}
return false;
}
/**
* Encodes the text into safe XML by replacing < > and & with XML tokens
*
* @param text the text
* @return the encoded text
*/
public static String xmlEncode(String text) {
if (text == null) {
return "";
}
// must replace amp first, so we dont replace < to amp later
text = replaceAll(text, "&", "&");
text = replaceAll(text, "\"", """);
text = replaceAll(text, "<", "<");
text = replaceAll(text, ">", ">");
return text;
}
/**
* Determines if the string has at least one letter in upper case
* @param text the text
* @return <tt>true</tt> if at least one letter is upper case, <tt>false</tt> otherwise
*/
public static boolean hasUpperCase(String text) {
if (text == null) {
return false;
}
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (Character.isUpperCase(ch)) {
return true;
}
}
return false;
}
/**
* Determines if the string is a fully qualified class name
*/
public static boolean isClassName(String text) {
boolean result = false;
if (text != null) {
String[] split = text.split("\\.");
if (split.length > 0) {
String lastToken = split[split.length - 1];
if (lastToken.length() > 0) {
result = Character.isUpperCase(lastToken.charAt(0));
}
}
}
return result;
}
/**
* Does the expression have the language start token?
*
* @param expression the expression
* @param language the name of the language, such as simple
* @return <tt>true</tt> if the expression contains the start token, <tt>false</tt> otherwise
*/
public static boolean hasStartToken(String expression, String language) {
if (expression == null) {
return false;
}
// for the simple language the expression start token could be "${"
if ("simple".equalsIgnoreCase(language) && expression.contains("${")) {
return true;
}
if (language != null && expression.contains("$" + language + "{")) {
return true;
}
return false;
}
/**
* Replaces all the from tokens in the given input string.
* <p/>
* This implementation is not recursive, not does it check for tokens in the replacement string.
*
* @param input the input string
* @param from the from string, must <b>not</b> be <tt>null</tt> or empty
* @param to the replacement string, must <b>not</b> be empty
* @return the replaced string, or the input string if no replacement was needed
* @throws IllegalArgumentException if the input arguments is invalid
*/
public static String replaceAll(String input, String from, String to) {
if (ObjectHelper.isEmpty(input)) {
return input;
}
if (from == null) {
throw new IllegalArgumentException("from cannot be null");
}
if (to == null) {
// to can be empty, so only check for null
throw new IllegalArgumentException("to cannot be null");
}
// fast check if there is any from at all
if (!input.contains(from)) {
return input;
}
final int len = from.length();
final int max = input.length();
StringBuilder sb = new StringBuilder(max);
for (int i = 0; i < max;) {
if (i + len <= max) {
String token = input.substring(i, i + len);
if (from.equals(token)) {
sb.append(to);
// fast forward
i = i + len;
continue;
}
}
// append single char
sb.append(input.charAt(i));
// forward to next
i++;
}
return sb.toString();
}
/**
* Creates a json tuple with the given name/value pair.
*
* @param name the name
* @param value the value
* @param isMap whether the tuple should be map
* @return the json
*/
public static String toJson(String name, String value, boolean isMap) {
if (isMap) {
return "{ " + doubleQuote(name) + ": " + doubleQuote(value) + " }";
} else {
return doubleQuote(name) + ": " + doubleQuote(value);
}
}
/**
* Asserts whether the string is <b>not</b> empty.
*
* @param value the string to test
* @param name the key that resolved the value
* @return the passed {@code value} as is
* @throws IllegalArgumentException is thrown if assertion fails
*/
public static String notEmpty(String value, String name) {
if (ObjectHelper.isEmpty(value)) {
throw new IllegalArgumentException(name + " must be specified and not empty");
}
return value;
}
/**
* Asserts whether the string is <b>not</b> empty.
*
* @param value the string to test
* @param on additional description to indicate where this problem occurred (appended as toString())
* @param name the key that resolved the value
* @return the passed {@code value} as is
* @throws IllegalArgumentException is thrown if assertion fails
*/
public static String notEmpty(String value, String name, Object on) {
if (on == null) {
ObjectHelper.notNull(value, name);
} else if (ObjectHelper.isEmpty(value)) {
throw new IllegalArgumentException(name + " must be specified and not empty on: " + on);
}
return value;
}
public static String[] splitOnCharacter(String value, String needle, int count) {
String rc[] = new String[count];
rc[0] = value;
for (int i = 1; i < count; i++) {
String v = rc[i - 1];
int p = v.indexOf(needle);
if (p < 0) {
return rc;
}
rc[i - 1] = v.substring(0, p);
rc[i] = v.substring(p + 1);
}
return rc;
}
/**
* Removes any starting characters on the given text which match the given
* character
*
* @param text the string
* @param ch the initial characters to remove
* @return either the original string or the new substring
*/
public static String removeStartingCharacters(String text, char ch) {
int idx = 0;
while (text.charAt(idx) == ch) {
idx++;
}
if (idx > 0) {
return text.substring(idx);
}
return text;
}
/**
* Capitalize the string (upper case first character)
*
* @param text the string
* @return the string capitalized (upper case first character)
*/
public static String capitalize(String text) {
if (text == null) {
return null;
}
int length = text.length();
if (length == 0) {
return text;
}
String answer = text.substring(0, 1).toUpperCase(Locale.ENGLISH);
if (length > 1) {
answer += text.substring(1, length);
}
return answer;
}
/**
* Returns the string after the given token
*
* @param text the text
* @param after the token
* @return the text after the token, or <tt>null</tt> if text does not contain the token
*/
public static String after(String text, String after) {
if (!text.contains(after)) {
return null;
}
return text.substring(text.indexOf(after) + after.length());
}
/**
* Returns an object after the given token
*
* @param text the text
* @param after the token
* @param mapper a mapping function to convert the string after the token to type T
* @return an Optional describing the result of applying a mapping function to the text after the token.
*/
public static <T> Optional<T> after(String text, String after, Function<String, T> mapper) {
String result = after(text, after);
if (result == null) {
return Optional.empty();
} else {
return Optional.ofNullable(mapper.apply(result));
}
}
/**
* Returns the string before the given token
*
* @param text the text
* @param before the token
* @return the text before the token, or <tt>null</tt> if text does not
* contain the token
*/
public static String before(String text, String before) {
if (!text.contains(before)) {
return null;
}
return text.substring(0, text.indexOf(before));
}
/**
* Returns an object before the given token
*
* @param text the text
* @param before the token
* @param mapper a mapping function to convert the string before the token to type T
* @return an Optional describing the result of applying a mapping function to the text before the token.
*/
public static <T> Optional<T> before(String text, String before, Function<String, T> mapper) {
String result = before(text, before);
if (result == null) {
return Optional.empty();
} else {
return Optional.ofNullable(mapper.apply(result));
}
}
/**
* Returns the string between the given tokens
*
* @param text the text
* @param after the before token
* @param before the after token
* @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens
*/
public static String between(String text, String after, String before) {
text = after(text, after);
if (text == null) {
return null;
}
return before(text, before);
}
/**
* Returns an object between the given token
*
* @param text the text
* @param after the before token
* @param before the after token
* @param mapper a mapping function to convert the string between the token to type T
* @return an Optional describing the result of applying a mapping function to the text between the token.
*/
public static <T> Optional<T> between(String text, String after, String before, Function<String, T> mapper) {
String result = between(text, after, before);
if (result == null) {
return Optional.empty();
} else {
return Optional.ofNullable(mapper.apply(result));
}
}
/**
* Returns the string between the most outer pair of tokens
* <p/>
* The number of token pairs must be evenly, eg there must be same number of before and after tokens, otherwise <tt>null</tt> is returned
* <p/>
* This implementation skips matching when the text is either single or double quoted.
* For example:
* <tt>${body.matches("foo('bar')")</tt>
* Will not match the parenthesis from the quoted text.
*
* @param text the text
* @param after the before token
* @param before the after token
* @return the text between the outer most tokens, or <tt>null</tt> if text does not contain the tokens
*/
public static String betweenOuterPair(String text, char before, char after) {
if (text == null) {
return null;
}
int pos = -1;
int pos2 = -1;
int count = 0;
int count2 = 0;
boolean singleQuoted = false;
boolean doubleQuoted = false;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (!doubleQuoted && ch == '\'') {
singleQuoted = !singleQuoted;
} else if (!singleQuoted && ch == '\"') {
doubleQuoted = !doubleQuoted;
}
if (singleQuoted || doubleQuoted) {
continue;
}
if (ch == before) {
count++;
} else if (ch == after) {
count2++;
}
if (ch == before && pos == -1) {
pos = i;
} else if (ch == after) {
pos2 = i;
}
}
if (pos == -1 || pos2 == -1) {
return null;
}
// must be even paris
if (count != count2) {
return null;
}
return text.substring(pos + 1, pos2);
}
/**
* Returns an object between the most outer pair of tokens
*
* @param text the text
* @param after the before token
* @param before the after token
* @param mapper a mapping function to convert the string between the most outer pair of tokens to type T
* @return an Optional describing the result of applying a mapping function to the text between the most outer pair of tokens.
*/
public static <T> Optional<T> betweenOuterPair(String text, char before, char after, Function<String, T> mapper) {
String result = betweenOuterPair(text, before, after);
if (result == null) {
return Optional.empty();
} else {
return Optional.ofNullable(mapper.apply(result));
}
}
/**
* Returns true if the given name is a valid java identifier
*/
public static boolean isJavaIdentifier(String name) {
if (name == null) {
return false;
}
int size = name.length();
if (size < 1) {
return false;
}
if (Character.isJavaIdentifierStart(name.charAt(0))) {
for (int i = 1; i < size; i++) {
if (!Character.isJavaIdentifierPart(name.charAt(i))) {
return false;
}
}
return true;
}
return false;
}
/**
* Cleans the string to a pure Java identifier so we can use it for loading class names.
* <p/>
* Especially from Spring DSL people can have \n \t or other characters that otherwise
* would result in ClassNotFoundException
*
* @param name the class name
* @return normalized classname that can be load by a class loader.
*/
public static String normalizeClassName(String name) {
StringBuilder sb = new StringBuilder(name.length());
for (char ch : name.toCharArray()) {
if (ch == '.' || ch == '[' || ch == ']' || ch == '-' || Character.isJavaIdentifierPart(ch)) {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Compares old and new text content and report back which lines are changed
*
* @param oldText the old text
* @param newText the new text
* @return a list of line numbers that are changed in the new text
*/
public static List<Integer> changedLines(String oldText, String newText) {
if (oldText == null || oldText.equals(newText)) {
return Collections.emptyList();
}
List<Integer> changed = new ArrayList<>();
String[] oldLines = oldText.split("\n");
String[] newLines = newText.split("\n");
for (int i = 0; i < newLines.length; i++) {
String newLine = newLines[i];
String oldLine = i < oldLines.length ? oldLines[i] : null;
if (oldLine == null) {
changed.add(i);
} else if (!newLine.equals(oldLine)) {
changed.add(i);
}
}
return changed;
}
/**
* Removes the leading and trailing whitespace and if the resulting
* string is empty returns {@code null}. Examples:
* <p>
* Examples:
* <blockquote><pre>
* trimToNull("abc") -> "abc"
* trimToNull(" abc") -> "abc"
* trimToNull(" abc ") -> "abc"
* trimToNull(" ") -> null
* trimToNull("") -> null
* </pre></blockquote>
*/
public static String trimToNull(final String given) {
if (given == null) {
return null;
}
final String trimmed = given.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
}