com.alibaba.nacos.common.utils.StringUtils Maven / Gradle / Ivy
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* 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 com.alibaba.nacos.common.utils;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
/**
* string util.
*
* @author Nacos
* @author zzq
*/
public class StringUtils {
private StringUtils() {
}
public static final String DOT = ".";
private static final int INDEX_NOT_FOUND = -1;
public static final String COMMA = ",";
public static final String EMPTY = "";
public static final String LF = "\n";
private static final String[] EMPTY_STRING_ARRAY = {};
private static final String TOP_PATH = "..";
private static final String FOLDER_SEPARATOR = "/";
private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
/**
* Create a string with encoding format as utf8.
*
* @param bytes the bytes that make up the string
* @return created string
*/
public static String newStringForUtf8(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
/**
* Checks if a string is empty (""), null and whitespace only.
*
* @param cs the string to check
* @return {@code true} if the string is empty and null and whitespace
*/
public static boolean isBlank(final 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;
}
/**
* Checks if a string is not empty (""), not null and not whitespace only.
*
* @param str the string to check, may be null
* @return {@code true} if the string is not empty and not null and not whitespace
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* Checks if a str is not empty ("") or not null.
*
* @param str the str to check, may be null
* @return {@code true} if the str is not empty or not null
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
/**
* Checks if a str is empty ("") or null.
*
* @param str the str to check, may be null
* @return {@code true} if the str is empty or null
*/
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
/**
* Returns either the passed in CharSequence, or if the CharSequence is
* empty or {@code null}, the value of {@code defaultStr}.
*
* @param str the CharSequence to check, may be null
* @param defaultStr the default CharSequence to return if the input is empty ("") or {@code null}, may be null
* @return the passed in CharSequence, or the default
*/
public static String defaultIfEmpty(String str, String defaultStr) {
return isEmpty(str) ? defaultStr : str;
}
/**
* Compares two CharSequences, returning {@code true} if they represent
* equal sequences of characters.
*
* @param str1 the first string, may be {@code null}
* @param str2 the second string, may be {@code null}
* @return {@code true} if the string are equal (case-sensitive), or both {@code null}
* @see Object#equals(Object)
*/
public static boolean equals(String str1, String str2) {
return str1 == null ? str2 == null : str1.equals(str2);
}
/**
* Removes control characters (char <= 32) from both
* ends of this String, handling {@code null} by returning {@code null}.
*
* @param str the String to be trimmed, may be null
* @return the trimmed string, {@code null} if null String input
*/
public static String trim(final String str) {
return str == null ? null : str.trim();
}
/**
* Substring between two index.
*
* @param str string
* @param open start index to sub
* @param close end index to sub
* @return substring
*/
public static String substringBetween(String str, String open, String close) {
if (str == null || open == null || close == null) {
return null;
}
int start = str.indexOf(open);
if (start != INDEX_NOT_FOUND) {
int end = str.indexOf(close, start + open.length());
if (end != INDEX_NOT_FOUND) {
return str.substring(start + open.length(), end);
}
}
return null;
}
/**
* Joins the elements of the provided array into a single String
* containing the provided list of elements.
*
* @param collection the Collection of values to join together, may be null
* @param separator the separator string to use
* @return the joined String, {@code null} if null array input
*/
public static String join(Collection collection, String separator) {
if (collection == null) {
return null;
}
StringBuilder stringBuilder = new StringBuilder();
Object[] objects = collection.toArray();
for (int i = 0; i < collection.size(); i++) {
if (objects[i] != null) {
stringBuilder.append(objects[i]);
if (i != collection.size() - 1 && separator != null) {
stringBuilder.append(separator);
}
}
}
return stringBuilder.toString();
}
public static String escapeJavaScript(String str) {
return escapeJavaStyleString(str, true, true);
}
private static String escapeJavaStyleString(String str, boolean escapeSingleQuotes, boolean escapeForwardSlash) {
if (str == null) {
return null;
}
try {
StringWriter writer = new StringWriter(str.length() * 2);
escapeJavaStyleString(writer, str, escapeSingleQuotes, escapeForwardSlash);
return writer.toString();
} catch (IOException ioe) {
// this should never ever happen while writing to a StringWriter
return null;
}
}
private static void escapeJavaStyleString(Writer out, String str, boolean escapeSingleQuote,
boolean escapeForwardSlash) throws IOException {
if (out == null) {
throw new IllegalArgumentException("The Writer must not be null");
}
if (str == null) {
return;
}
int sz;
sz = str.length();
for (int i = 0; i < sz; i++) {
char ch = str.charAt(i);
// handle unicode
if (ch > 0xfff) {
out.write("\\u" + hex(ch));
} else if (ch > 0xff) {
out.write("\\u0" + hex(ch));
} else if (ch > 0x7f) {
out.write("\\u00" + hex(ch));
} else if (ch < 32) {
switch (ch) {
case '\b':
out.write('\\');
out.write('b');
break;
case '\n':
out.write('\\');
out.write('n');
break;
case '\t':
out.write('\\');
out.write('t');
break;
case '\f':
out.write('\\');
out.write('f');
break;
case '\r':
out.write('\\');
out.write('r');
break;
default:
if (ch > 0xf) {
out.write("\\u00" + hex(ch));
} else {
out.write("\\u000" + hex(ch));
}
break;
}
} else {
switch (ch) {
case '\'':
if (escapeSingleQuote) {
out.write('\\');
}
out.write('\'');
break;
case '"':
out.write('\\');
out.write('"');
break;
case '\\':
out.write('\\');
out.write('\\');
break;
case '/':
if (escapeForwardSlash) {
out.write('\\');
}
out.write('/');
break;
default:
out.write(ch);
break;
}
}
}
}
private static String hex(char ch) {
return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
}
/**
* Checks if CharSequence contains a search CharSequence irrespective of case, handling {@code null}.
* Case-insensitivity is defined as by {@link String#equalsIgnoreCase(String)}.
*
* A {@code null} CharSequence will return {@code false}.
*
* @param str the CharSequence to check, may be null
* @param searchStr the CharSequence to find, may be null
* @return true if the CharSequence contains the search CharSequence irrespective of case or false if not or {@code
* null} string input
*/
public static boolean containsIgnoreCase(final CharSequence str, final CharSequence searchStr) {
if (str == null || searchStr == null) {
return false;
}
String str1 = str.toString().toLowerCase();
String str2 = searchStr.toString().toLowerCase();
return str1.contains(str2);
}
/**
* Checks if CharSequence contains a search CharSequence.
*
* @param str the CharSequence to check, may be null
* @param searchStr the CharSequence to find, may be null
* @return true if the CharSequence contains the search CharSequence
*/
public static boolean contains(final CharSequence str, final CharSequence searchStr) {
if (str == null || searchStr == null) {
return false;
}
return str.toString().contains(searchStr);
}
/**
* Checks if none of the CharSequences are blank ("") or null and whitespace only..
*
* @param css the CharSequences to check, may be null or empty
* @return {@code true} if none of the CharSequences are blank or null or whitespace only
*/
public static boolean isNoneBlank(final CharSequence... css) {
return !isAnyBlank(css);
}
/**
* Checks if any one of the CharSequences are blank ("") or null and not whitespace only..
*
* @param css the CharSequences to check, may be null or empty
* @return {@code true} if any of the CharSequences are blank or null or whitespace only
*/
public static boolean isAnyBlank(final CharSequence... css) {
if (ArrayUtils.isEmpty(css)) {
return true;
}
for (final CharSequence cs : css) {
if (isBlank(cs)) {
return true;
}
}
return false;
}
/**
* Check if a CharSequence starts with a specified prefix.
*
* {@code null}s are handled without exceptions. Two {@code null}
* references are considered to be equal. The comparison is case sensitive.
*
* @param str the CharSequence to check, may be null
* @param prefix the prefix to find, may be null
* @return {@code true} if the CharSequence starts with the prefix, case sensitive, or both {@code null}
* @see java.lang.String#startsWith(String)
*/
public static boolean startsWith(final CharSequence str, final CharSequence prefix) {
return startsWith(str, prefix, false);
}
/**
* Check if a CharSequence starts with a specified prefix (optionally case insensitive).
*
* @param str the CharSequence to check, may be null
* @param prefix the prefix to find, may be null
* @param ignoreCase indicates whether the compare should ignore case (case insensitive) or not.
* @return {@code true} if the CharSequence starts with the prefix or both {@code null}
* @see java.lang.String#startsWith(String)
*/
private static boolean startsWith(final CharSequence str, final CharSequence prefix, final boolean ignoreCase) {
if (str == null || prefix == null) {
return str == null && prefix == null;
}
if (prefix.length() > str.length()) {
return false;
}
if (ignoreCase) {
String lowerCaseStr = str.toString().toLowerCase();
String lowerCasePrefix = str.toString().toLowerCase();
return lowerCaseStr.startsWith(lowerCasePrefix);
} else {
return str.toString().startsWith(prefix.toString());
}
}
/**
* Case insensitive check if a CharSequence starts with a specified prefix.
*
* {@code null}s are handled without exceptions. Two {@code null}
* references are considered to be equal. The comparison is case insensitive.
*
* @param str the CharSequence to check, may be null
* @param prefix the prefix to find, may be null
* @return {@code true} if the CharSequence starts with the prefix, case insensitive, or both {@code null}
* @see java.lang.String#startsWith(String)
*/
public static boolean startsWithIgnoreCase(final CharSequence str, final CharSequence prefix) {
return startsWith(str, prefix, true);
}
/**
* Deletes all whitespaces from a String as defined by
* {@link Character#isWhitespace(char)}.
*
* @param str the String to delete whitespace from, may be null
* @return the String without whitespaces, null
if null String input
*/
public static String deleteWhitespace(String str) {
if (isEmpty(str)) {
return str;
}
int sz = str.length();
char[] chs = new char[sz];
int count = 0;
for (int i = 0; i < sz; i++) {
if (!Character.isWhitespace(str.charAt(i))) {
chs[count++] = str.charAt(i);
}
}
if (count == sz) {
return str;
}
return new String(chs, 0, count);
}
/**
* Compares two CharSequences, returning {@code true} if they represent
* equal sequences of characters, ignoring case.
*
* @param str1 the first string, may be null
* @param str2 the second string, may be null
* @return {@code true} if the string are equal, case insensitive, or both {@code null}
*/
public static boolean equalsIgnoreCase(String str1, String str2) {
return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2);
}
/**
* Splits the provided text into an array with a maximum length, separators specified. If separatorChars is empty,
* divide by blank.
*
* @param str the String to parse, may be null
* @return an array of parsed Strings
*/
@SuppressWarnings("checkstyle:WhitespaceAround")
public static String[] split(final String str, String separatorChars) {
if (str == null) {
return null;
}
if (str.length() == 0) {
return new String[0];
}
if (separatorChars == null) {
separatorChars = " +";
}
return str.split(separatorChars);
}
private static String[] tokenizeLocaleSource(String localeSource) {
return tokenizeToStringArray(localeSource, "_ ", false, false);
}
/**
* Tokenize the given {@code String} into a {@code String} array via a {@link StringTokenizer}.
*
* The given {@code delimiters} string can consist of any number of
* delimiter characters. Each of those characters can be used to separate tokens. A delimiter is always a single
* character;
*
* @param str the {@code String} to tokenize (potentially {@code null} or empty)
* @param delimiters the delimiter characters, assembled as a {@code String} (each of the characters is
* individually considered as a delimiter)
* @param trimTokens trim the tokens via {@link String#trim()}
* @param ignoreEmptyTokens omit empty tokens from the result array (only applies to tokens that are empty after
* trimming; StringTokenizer will not consider subsequent delimiters as token in the first
* place).
* @return an array of the tokens
* @see java.util.StringTokenizer
* @see String#trim()
*/
public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens,
boolean ignoreEmptyTokens) {
if (str == null) {
return EMPTY_STRING_ARRAY;
}
StringTokenizer st = new StringTokenizer(str, delimiters);
List tokens = new ArrayList<>();
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (trimTokens) {
token = token.trim();
}
if (!ignoreEmptyTokens || token.length() > 0) {
tokens.add(token);
}
}
return toStringArray(tokens);
}
/**
* Copy the given {@link Collection} into a {@code String} array.
*
* The {@code Collection} must contain {@code String} elements only.
*
* @param collection the {@code Collection} to copy (potentially {@code null} or empty)
* @return the resulting {@code String} array
*/
public static String[] toStringArray(Collection collection) {
return (!CollectionUtils.isEmpty(collection) ? collection.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY);
}
/**
* Check whether the given {@code String} contains actual text.
*
* More specifically, this method returns {@code true} if the
* {@code String} is not {@code null}, its length is greater than 0, and it contains at least one non-whitespace
* character.
*
* @param str the {@code String} to check (may be {@code null})
* @return {@code true} if the {@code String} is not {@code null}, its length is greater than 0, and it does not
* contain whitespace only
* @see Character#isWhitespace
*/
public static boolean hasText(String str) {
return (str != null && !str.isEmpty() && containsText(str));
}
private static boolean containsText(CharSequence str) {
int strLen = str.length();
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(str.charAt(i))) {
return true;
}
}
return false;
}
/**
* Normalize the path by suppressing sequences like "path/.." and inner simple dots.
*
*
The result is convenient for path comparison. For other uses,
* notice that Windows separators ("\") are replaced by simple slashes.
*
*
NOTE that {@code cleanPath} should not be depended
* upon in a security context. Other mechanisms should be used to prevent path-traversal issues.
*
* @param path the original path
* @return the normalized path
*/
public static String cleanPath(String path) {
if (!hasLength(path)) {
return path;
}
String normalizedPath = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
String pathToUse = normalizedPath;
// Shortcut if there is no work to do
if (pathToUse.indexOf(DOT) == -1) {
return pathToUse;
}
// Strip prefix from path to analyze, to not treat it as part of the
// first path element. This is necessary to correctly parse paths like
// "file:core/../core/io/Resource.class", where the ".." should just
// strip the first "core" directory while keeping the "file:" prefix.
int prefixIndex = pathToUse.indexOf(':');
String prefix = "";
if (prefixIndex != -1) {
prefix = pathToUse.substring(0, prefixIndex + 1);
if (prefix.contains(FOLDER_SEPARATOR)) {
prefix = "";
} else {
pathToUse = pathToUse.substring(prefixIndex + 1);
}
}
if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
prefix = prefix + FOLDER_SEPARATOR;
pathToUse = pathToUse.substring(1);
}
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
// we never require more elements than pathArray and in the common case the same number
Deque pathElements = new ArrayDeque<>(pathArray.length);
int tops = 0;
for (int i = pathArray.length - 1; i >= 0; i--) {
String element = pathArray[i];
if (DOT.equals(element)) {
// Points to current directory - drop it.
} else if (TOP_PATH.equals(element)) {
// Registering top path found.
tops++;
} else {
if (tops > 0) {
// Merging path element with element corresponding to top path.
tops--;
} else {
// Normal path element found.
pathElements.addFirst(element);
}
}
}
// All path elements stayed the same - shortcut
if (pathArray.length == pathElements.size()) {
return normalizedPath;
}
// Remaining top paths need to be retained.
for (int i = 0; i < tops; i++) {
pathElements.addFirst(TOP_PATH);
}
// If nothing else left, at least explicitly point to current path.
if (pathElements.size() == 1 && pathElements.getLast().isEmpty() && !prefix.endsWith(FOLDER_SEPARATOR)) {
pathElements.addFirst(DOT);
}
final String joined = collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
// avoid string concatenation with empty prefix
return prefix.isEmpty() ? joined : prefix + joined;
}
/**
* Convert a {@code Collection} into a delimited {@code String} (e.g. CSV).
*
* Useful for {@code toString()} implementations.
*
* @param coll the {@code Collection} to convert (potentially {@code null} or empty)
* @param delim the delimiter to use (typically a ",")
* @return the delimited {@code String}
*/
public static String collectionToDelimitedString(Collection> coll, String delim) {
return collectionToDelimitedString(coll, delim, "", "");
}
/**
* Convert a {@link Collection} to a delimited {@code String} (e.g. CSV).
*
*
Useful for {@code toString()} implementations.
*
* @param coll the {@code Collection} to convert (potentially {@code null} or empty)
* @param delim the delimiter to use (typically a ",")
* @param prefix the {@code String} to start each element with
* @param suffix the {@code String} to end each element with
* @return the delimited {@code String}
*/
public static String collectionToDelimitedString(Collection> coll, String delim, String prefix, String suffix) {
if (CollectionUtils.isEmpty(coll)) {
return "";
}
int totalLength = coll.size() * (prefix.length() + suffix.length()) + (coll.size() - 1) * delim.length();
for (Object element : coll) {
totalLength += String.valueOf(element).length();
}
StringBuilder sb = new StringBuilder(totalLength);
Iterator> it = coll.iterator();
while (it.hasNext()) {
sb.append(prefix).append(it.next()).append(suffix);
if (it.hasNext()) {
sb.append(delim);
}
}
return sb.toString();
}
/**
* Check that the given {@code String} is neither {@code null} nor of length 0.
*
*
Note: this method returns {@code true} for a {@code String} that
* purely consists of whitespace.
*
* @param str the {@code String} to check (may be {@code null})
* @return {@code true} if the {@code String} is not {@code null} and has length
* @see #hasText(String)
*/
public static boolean hasLength(String str) {
return (str != null && !str.isEmpty());
}
/**
* Take a {@code String} that is a delimited list and convert it into a {@code String} array.
*
*
A single {@code delimiter} may consist of more than one character,
* but it will still be considered as a single delimiter string, rather than as bunch of potential delimiter
* characters, in contrast to {@link #tokenizeToStringArray}.
*
* @param str the input {@code String} (potentially {@code null} or empty)
* @param delimiter the delimiter between elements (this is a single delimiter, rather than a bunch individual
* delimiter characters)
* @return an array of the tokens in the list
* @see #tokenizeToStringArray
*/
public static String[] delimitedListToStringArray(String str, String delimiter) {
return delimitedListToStringArray(str, delimiter, null);
}
/**
* Take a {@code String} that is a delimited list and convert it into a {@code String} array.
*
*
A single {@code delimiter} may consist of more than one character,
* but it will still be considered as a single delimiter string, rather than as bunch of potential delimiter
* characters, in contrast to {@link #tokenizeToStringArray}.
*
* @param str the input {@code String} (potentially {@code null} or empty)
* @param delimiter the delimiter between elements (this is a single delimiter, rather than a bunch individual
* delimiter characters)
* @param charsToDelete a set of characters to delete; useful for deleting unwanted line breaks: e.g. "\r\n\f" will
* delete all new lines and line feeds in a {@code String}
* @return an array of the tokens in the list
* @see #tokenizeToStringArray
*/
public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) {
if (str == null) {
return EMPTY_STRING_ARRAY;
}
if (delimiter == null) {
return new String[] {str};
}
List result = new ArrayList<>();
if (delimiter.isEmpty()) {
for (int i = 0; i < str.length(); i++) {
result.add(deleteAny(str.substring(i, i + 1), charsToDelete));
}
} else {
int pos = 0;
int delPos;
while ((delPos = str.indexOf(delimiter, pos)) != -1) {
result.add(deleteAny(str.substring(pos, delPos), charsToDelete));
pos = delPos + delimiter.length();
}
if (str.length() > 0 && pos <= str.length()) {
// Add rest of String, but not in case of empty input.
result.add(deleteAny(str.substring(pos), charsToDelete));
}
}
return toStringArray(result);
}
/**
* Delete any character in a given {@code String}.
*
* @param inString the original {@code String}
* @param charsToDelete a set of characters to delete. E.g. "az\n" will delete 'a's, 'z's and new lines.
* @return the resulting {@code String}
*/
public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) {
return inString;
}
int lastCharIndex = 0;
char[] result = new char[inString.length()];
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt(i);
if (charsToDelete.indexOf(c) == -1) {
result[lastCharIndex++] = c;
}
}
if (lastCharIndex == inString.length()) {
return inString;
}
return new String(result, 0, lastCharIndex);
}
/**
* Replace all occurrences of a substring within a string with another string.
*
* @param inString {@code String} to examine
* @param oldPattern {@code String} to replace
* @param newPattern {@code String} to insert
* @return a {@code String} with the replacements
*/
public static String replace(String inString, String oldPattern, String newPattern) {
if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) {
return inString;
}
int index = inString.indexOf(oldPattern);
if (index == -1) {
// no occurrence -> can return input as-is
return inString;
}
int capacity = inString.length();
if (newPattern.length() > oldPattern.length()) {
capacity += 16;
}
StringBuilder sb = new StringBuilder(capacity);
int pos = 0;
int patLen = oldPattern.length();
while (index >= 0) {
sb.append(inString, pos, index);
sb.append(newPattern);
pos = index + patLen;
index = inString.indexOf(oldPattern, pos);
}
// append any characters to the right of a match
sb.append(inString, pos, inString.length());
return sb.toString();
}
/**
* Apply the given relative path to the given Java resource path, assuming standard Java folder separation (i.e. "/"
* separators).
*
* @param path the path to start from (usually a full file path)
* @param relativePath the relative path to apply (relative to the full file path above)
* @return the full file path that results from applying the relative path
*/
public static String applyRelativePath(String path, String relativePath) {
int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
if (separatorIndex != -1) {
String newPath = path.substring(0, separatorIndex);
if (!relativePath.startsWith(FOLDER_SEPARATOR)) {
newPath += FOLDER_SEPARATOR;
}
return newPath + relativePath;
} else {
return relativePath;
}
}
/**
* Extract the filename from the given Java resource path, e.g. {@code "mypath/myfile.txt" → "myfile.txt"}.
*
* @param path the file path (may be {@code null})
* @return the extracted filename, or {@code null} if none
*/
public static String getFilename(String path) {
if (path == null) {
return null;
}
int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path);
}
/**
* Capitalize a {@code String}, changing the first letter to upper case as per {@link Character#toUpperCase(char)}.
* No other letters are changed.
*
* @param str the {@code String} to capitalize
* @return the capitalized {@code String}
*/
public static String capitalize(String str) {
return changeFirstCharacterCase(str, true);
}
private static String changeFirstCharacterCase(String str, boolean capitalize) {
if (!hasLength(str)) {
return str;
}
char baseChar = str.charAt(0);
char updatedChar;
if (capitalize) {
updatedChar = Character.toUpperCase(baseChar);
} else {
updatedChar = Character.toLowerCase(baseChar);
}
if (baseChar == updatedChar) {
return str;
}
char[] chars = str.toCharArray();
chars[0] = updatedChar;
return new String(chars);
}
}