StringUtils.java
/*-
* ========================LICENSE_START=================================
* flyway-core
* ========================================================================
* Copyright (C) 2010 - 2025 Red Gate Software 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.
* =========================LICENSE_END==================================
*/
package org.flywaydb.core.internal.util;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.flywaydb.core.internal.configuration.ConfigUtils;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StringUtils {
private static final String WHITESPACE_CHARS = " \t\n\f\r";
/**
* Trims or pads (with spaces) this string, so it has this exact length.
*
* @param str The string to adjust. {@code null} is treated as an empty string.
* @param length The exact length to reach.
* @return The adjusted string.
*/
public static String trimOrPad(String str, int length) {
return trimOrPad(str, length, ' ');
}
/**
* Trims or pads this string, so it has this exact length.
*
* @param str The string to adjust. {@code null} is treated as an empty string.
* @param length The exact length to reach.
* @param padChar The padding character.
* @return The adjusted string.
*/
public static String trimOrPad(String str, int length, char padChar) {
StringBuilder result;
if (str == null) {
result = new StringBuilder();
} else {
result = new StringBuilder(str);
}
if (result.length() > length) {
return result.substring(0, length);
}
while (result.length() < length) {
result.append(padChar);
}
return result.toString();
}
/**
* Trims or pads this string, so it has this exact length.
*
* @param str The string to adjust. {@code null} is treated as an empty string.
* @param length The exact length to reach.
* @param padChar The padding character.
* @return The adjusted string.
*/
public static String trimOrLeftPad(String str, int length, char padChar) {
if (str == null) {
str = "";
}
if (str.length() > length) {
return str.substring(0, length);
}
return leftPad(str, length, padChar);
}
public static String leftPad(String original, int length, char padChar) {
StringBuilder result = new StringBuilder(original);
while (result.length() < length) {
result.insert(0, padChar);
}
return result.toString();
}
public static String rightPad(String original, int length, char padChar) {
StringBuilder result = new StringBuilder(original);
while (result.length() < length) {
result.append(padChar);
}
return result.toString();
}
/**
* Replaces all sequences of whitespace by a single blank. Ex.: " " -> " "
*
* @param str The string to analyse.
* @return The input string, with all whitespace collapsed.
*/
public static String collapseWhitespace(String str) {
StringBuilder result = new StringBuilder();
char previous = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (isCharAnyOf(c, WHITESPACE_CHARS)) {
if (previous != ' ') {
result.append(' ');
}
previous = ' ';
} else {
result.append(c);
previous = c;
}
}
return result.toString();
}
/**
* Returns the first n characters from this string, where n = count. If the string is shorter, the entire string
* will be returned. If the string is longer, it will be truncated.
*
* @param str The string to parse.
* @param count The amount of characters to return.
* @return The first n characters from this string, where n = count.
*/
public static String left(String str, int count) {
if (str == null) {
return null;
}
if (str.length() < count) {
return str;
}
return str.substring(0, count);
}
/**
* Replaces all occurrences of this originalToken in this string with this replacementToken.
*
* @param str The string to process.
* @param originalToken The token to replace.
* @param replacementToken The replacement.
* @return The transformed str.
*/
public static String replaceAll(String str, String originalToken, String replacementToken) {
return str.replaceAll(Pattern.quote(originalToken), Matcher.quoteReplacement(replacementToken));
}
/**
* Checks whether this string is not {@code null} and not <i>empty</i>.
*
* @param str The string to check.
* @return {@code true} if it has content, {@code false} if it is {@code null} or blank.
*/
public static boolean hasLength(String str) {
return str != null && str.length() > 0;
}
/**
* Turns this string array in one comma-delimited string.
*
* @param strings The array to process.
* @return The new comma-delimited string. An empty string if {@code strings} is empty. {@code null} if strings is {@code null}.
*/
public static String arrayToCommaDelimitedString(Object[] strings) {
return arrayToDelimitedString(",", strings);
}
/**
* Turns this string array in one delimited string.
*
* @param delimiter The delimiter to use.
* @param strings The array to process.
* @return The new delimited string. An empty string if {@code strings} is empty. {@code null} if strings is {@code null}.
*/
public static String arrayToDelimitedString(String delimiter, Object[] strings) {
if (strings == null) {
return null;
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
if (i > 0) {
builder.append(delimiter);
}
builder.append(strings[i]);
}
return builder.toString();
}
/**
* Checks whether this string isn't {@code null} and contains at least one non-blank character.
*
* @param s The string to check.
* @return {@code true} if it has text, {@code false} if not.
*/
public static boolean hasText(String s) {
return (s != null) && (s.trim().length() > 0);
}
/**
* Splits this string into an array using this delimiter.
*
* @param str The string to split.
* @param delimiter The delimiter to use.
* @return The resulting array.
*/
public static String[] tokenizeToStringArray(String str, String delimiter) {
if (str == null) {
return null;
}
return tokenizeToStringCollection(str, delimiter).toArray(new String[0]);
}
/**
* Splits this string into a collection using this delimiter.
*
* @param str The string to split.
* @param delimiter The delimiter to use.
* @return The resulting array.
*/
public static List<String> tokenizeToStringCollection(String str, String delimiter) {
if (str == null) {
return null;
}
return Arrays.stream(str.split(delimiter)).map(String::trim).collect(Collectors.toList());
}
/**
* Convenience method to return a Collection as a comma-delimited
* String. e.g. useful for {@code toString()} implementations.
*
* @param collection the Collection to analyse
* @return The comma-delimited String.
*/
public static String collectionToCommaDelimitedString(Collection<?> collection) {
return collectionToDelimitedString(collection, ", ");
}
/**
* Convenience method to return a Collection as a delimited
* String. E.g. useful for {@code toString()} implementations.
*
* @param collection the Collection to analyse
* @param delimiter The delimiter.
* @return The delimited String.
*/
public static String collectionToDelimitedString(Collection<?> collection, String delimiter) {
if (collection == null) {
return "";
}
StringBuilder sb = new StringBuilder();
Iterator it = collection.iterator();
while (it.hasNext()) {
sb.append(it.next());
if (it.hasNext()) {
sb.append(delimiter);
}
}
return sb.toString();
}
/**
* Trim any leading occurrence of this character from the given String.
*
* @param str the String to check.
* @param character The character to trim.
* @return the trimmed String
* @see java.lang.Character#isWhitespace
*/
public static String trimLeadingCharacter(String str, char character) {
StringBuilder buf = new StringBuilder(str);
while (buf.length() > 0 && character == buf.charAt(0)) {
buf.deleteCharAt(0);
}
return buf.toString();
}
/**
* Checks whether this strings both begins with this prefix and ends withs either of these suffixes.
*
* @param str The string to check.
* @param prefix The prefix.
* @param suffixes The suffixes.
* @return {@code true} if it does, {@code false} if not.
*/
public static boolean startsAndEndsWith(String str, String prefix, String... suffixes) {
if (StringUtils.hasLength(prefix) && !str.startsWith(prefix)) {
return false;
}
for (String suffix : suffixes) {
if (str.toUpperCase().endsWith(suffix.toUpperCase()) && (str.length() > (prefix + suffix).length())) {
return true;
}
}
return false;
}
/**
* Wrap this string every lineSize characters.
*
* @param str The string to wrap.
* @param lineSize The maximum size of each line.
* @return The wrapped string.
*/
public static String wrap(String str, int lineSize) {
if (str.length() < lineSize) {
return str;
}
StringBuilder result = new StringBuilder();
int oldPos = 0;
for (int pos = lineSize; pos < str.length(); pos += lineSize) {
result.append(str, oldPos, pos).append("\n");
oldPos = pos;
}
result.append(str.substring(oldPos));
return result.toString();
}
/**
* Wrap this string at the word boundary at or below lineSize characters.
*
* @param str The string to wrap.
* @param lineSize The maximum size of each line.
* @return The word-wrapped string.
*/
public static String wordWrap(String str, int lineSize) {
if (str.length() < lineSize) {
return str;
}
StringBuilder result = new StringBuilder();
int oldPos = 0;
int pos = lineSize;
while (pos < str.length()) {
if (Character.isWhitespace(str.charAt(pos))) {
pos++;
continue;
}
String part = str.substring(oldPos, pos);
int spacePos = part.lastIndexOf(' ');
if (spacePos > 0) {
pos = oldPos + spacePos + 1;
}
result.append(str.substring(oldPos, pos).trim()).append("\n");
oldPos = pos;
pos += lineSize;
}
result.append(str.substring(oldPos));
return result.toString();
}
/**
* Checks whether this character matches any of these characters.
*
* @param c The char to check.
* @param chars The chars that should match.
* @return {@code true} if it does, {@code false if not}.
*/
public static boolean isCharAnyOf(char c, String chars) {
for (int i = 0; i < chars.length(); i++) {
if (chars.charAt(i) == c) {
return true;
}
}
return false;
}
public static Pair<String,String> getFileNameAndExtension(String path) {
String[] foldersSplit = path.split("[|/\\\\]");
String fileNameAndExtension = foldersSplit[foldersSplit.length - 1];
String[] nameExtensionSplit = fileNameAndExtension.split("\\.");
if (nameExtensionSplit.length < 2) {
return Pair.of(fileNameAndExtension, "");
}
return Pair.of(nameExtensionSplit[nameExtensionSplit.length - 2], nameExtensionSplit[nameExtensionSplit.length - 1]);
}
public static Pair<String, String> splitAtFirstSeparator(String input, String separator) {
int separatorIndex = input.indexOf(separator);
if (separatorIndex >= 0) {
return Pair.of(input.substring(0, separatorIndex),
input.substring(separatorIndex + separator.length()));
} else {
return Pair.of(input, "");
}
}
public static int countOccurrencesOf(String text, char search) {
int count = 0;
for (char c : text.toCharArray()) {
if (c == search) {
count++;
}
}
return count;
}
public static String getDaysString(long days) {
return days + " day" + pluralizeSuffix(days);
}
public static String pluralizeSuffix(long input) {
return input != 1 ? "s" : "";
}
public static String capitalizeFirstLetter(String str) {
if(!hasText(str)) {
return "";
}
String result = str.substring(0, 1).toUpperCase();
if (str.length() > 1) {
result += str.substring(1);
}
return result;
}
public static String redactedValueStringOfAMap(String value) {
if (!hasText(value) ||
!value.startsWith("{") ||
!value.endsWith("}") ||
value.length() <= 2) {
return value;
}
value = value.substring(1, value.length() - 1).trim();
String[] pairs = value.split(",");
StringBuilder resultBuilder = new StringBuilder();
for (String s : pairs) {
String[] pair = s.trim().split("=");
if(pair.length != 2) {
continue;
}
pair[0] = pair[0].trim();
pair[1] = pair[1].trim();
pair[1] = redactValueIfSensitive(pair[0], pair[1]);
resultBuilder.append(pair[0]).append("=").append(pair[1]).append(",").append(" ");
}
String result = resultBuilder.toString().trim();
if (result.endsWith(",")) {
result = result.substring(0, result.length() - 1);
}
return "{" + result + "}";
}
public static String redactValueIfSensitive(String key, String value) {
if (!hasText(key) || !hasText(value)) {
return value;
}
if (key.toLowerCase().endsWith("password") || key.toLowerCase().endsWith("token")
|| ConfigUtils.LICENSE_KEY.equalsIgnoreCase(key)) {
value = "********";
}
return value;
}
}