/** * Licensed under the GNU LGPL v.2.1 or later. */ package info.freelibrary.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Provides a few convenience methods for working with strings. * * @author <a href="mailto:ksclarke@ksclarke.io">Kevin S. Clarke</a> */ public class StringUtils { public static final String UTF_8 = "UTF-8"; private StringUtils() { } /** * Trims a string; if there is nothing left after the trimming, returns null. If the passed in object is not a * string, a cast exception will be thrown. * * @param aString The string to be trimmed * @return The trimmed string or null if string is empty */ public static String trimToNull(final String aString) { return trimTo(aString, null); } /** * Trims a string object down into a boolean and has the ability to define what the default value should be. We only * offer the method with the default value because most times a boolean with either exist or not (and in the case of * not a default should be specified). * * @param aString A boolean in string form * @param aBool A default boolean value * @return The boolean representation of the string value or the default value */ public static boolean trimToBool(final String aString, final boolean aBool) { final String boolString = trimTo(aString, Boolean.toString(aBool)); return Boolean.parseBoolean(boolString); } /** * Trims a string; if there is nothing left after the trimming, returns whatever the default value passed in is. * * @param aString The string to be trimmed * @param aDefault A default string to return if a null string is passed in * @return The trimmed string or the default value if string is empty */ public static String trimTo(final String aString, final String aDefault) { if (aString == null) { return aDefault; } final String trimmed = aString.trim(); return trimmed.length() == 0 ? aDefault : trimmed; } /** * Formats the supplied message using the supplied details. The string form of the detail comes from the object's * <code>toString()</code> method. * * @param aMessage A message to format * @param aDetail Additional details to integrate into the message * @return The formatted message */ public static String format(final String aMessage, final Object... aDetail) { return format(aMessage, StringUtils.toStrings(aDetail)); } /** * Takes a <code>String</code> in the form "This is {} text {}" and replaces the <code>{}</code>s with values from * the supplied <code>String[]</code>. The number of curly braces should be the same as the number of strings in the * string array. * * @param aMessage A string that contains curly braces in the form <code>{}</code> * @param aDetails Strings that should be put in place of the curly braces in the message string. * @return The formatted string */ public static String format(final String aMessage, final String... aDetails) { int position = 0; int count = 0; while ((position = aMessage.indexOf("{}", position)) != -1) { position += 1; count += 1; } if (count != aDetails.length) { throw new IndexOutOfBoundsException("Different number of slots and values: " + count + " and " + aDetails.length); } final String[] parts = aMessage.split("\\{\\}"); final StringBuilder builder = new StringBuilder(); if (count == 1 && parts.length == 0) { builder.append(aDetails[0]); } else { for (int index = 0; index < parts.length; index++) { builder.append(parts[index]); if (index < aDetails.length) { builder.append(aDetails[index]); } } } return builder.length() == 0 ? aMessage : builder.toString(); } /** * Converts a varargs into an array of strings by calling <code>toString()</code> on each object. * * @param aVarargs A varargs of objects * @return An array of strings */ public static String[] toStrings(final Object... aVarargs) { final String[] strings = new String[aVarargs.length]; for (int index = 0; index < strings.length; index++) { strings[index] = aVarargs[index].toString(); } return strings; } /** * Normalizes white space in the message value. * * @param aMessage A message * @return The message with white space normalized */ public static String normalizeWS(final String aMessage) { return aMessage.replaceAll("\\s+", " "); } /** * Pads the beginning of a supplied string with the repetition of a supplied value. * * @param aString The string to pad * @param aPadding The string to be repeated as the padding * @param aRepeatCount How many times to repeat the padding * @return The front padded string */ public static String padStart(final String aString, final String aPadding, final int aRepeatCount) { if (aRepeatCount != 0) { final StringBuilder buffer = new StringBuilder(); for (int index = 0; index < aRepeatCount; index++) { buffer.append(aPadding); } return buffer.append(aString).toString(); } return aString; } /** * Pads the end of a supplied string with the repetition of a supplied value. * * @param aString The string to pad * @param aPadding The string to be repeated as the padding * @param aRepeatCount How many times to repeat the padding * @return The end padded string */ public static String padEnd(final String aString, final String aPadding, final int aRepeatCount) { if (aRepeatCount != 0) { final StringBuilder buffer = new StringBuilder(aString); for (int index = 0; index < aRepeatCount; index++) { buffer.append(aPadding); } return buffer.toString(); } return aString; } /** * Formats a string with or without line breaks into a string with lines with less than a supplied number of * characters per line. * * @param aString A string to format * @param aCount A number of characters to allow per line * @return A string formatted using the supplied count */ public static String toCharCount(final String aString, final int aCount) { final StringBuilder builder = new StringBuilder(); final String[] words = aString.split("\\s"); int count = 0; for (final String word : words) { count += word.length(); if (count < aCount) { builder.append(word); if ((count += 1) < aCount) { builder.append(' '); } else { builder.append("\r\n "); count = 2; } } else { builder.append("\r\n ").append(word); count = word.length() + 2; // two spaces at start of line } } return builder.toString(); } /** * Creates a new string from the repetition of a supplied value. * * @param aValue The string to repeat, creating a new string * @param aRepeatCount The number of times to repeat the supplied value * @return The new string containing the supplied value repeated the specified number of times */ public static String repeat(final String aValue, final int aRepeatCount) { final StringBuilder buffer = new StringBuilder(); for (int index = 0; index < aRepeatCount; index++) { buffer.append(aValue); } return buffer.toString(); } /** * Creates a new string from the repetition of a supplied char. * * @param aChar The char to repeat, creating a new string * @param aRepeatCount The number of times to repeat the supplied value * @return The new string containing the supplied value repeated the specified number of times */ public static String repeat(final char aChar, final int aRepeatCount) { final StringBuilder buffer = new StringBuilder(); for (int index = 0; index < aRepeatCount; index++) { buffer.append(aChar); } return buffer.toString(); } /** * Reads the contents of a file using the supplied {@link Charset}; the default system charset may vary across * systems so can't be trusted. * * @param aFile The file from which to read * @param aCharsetName The name of the character set of the file to be read * @return The information read from the file * @throws IOException If the supplied file could not be read */ public static String read(final File aFile, final String aCharsetName) throws IOException { return read(aFile, Charset.forName(aCharsetName)); } /** * Reads the contents of a file using the supplied {@link Charset}; the default system charset may vary across * systems so can't be trusted. * * @param aFile The file from which to read * @param aCharset The character set of the file to be read * @return The information read from the file * @throws IOException If the supplied file could not be read */ public static String read(final File aFile, final Charset aCharset) throws IOException { String string = new String(readBytes(aFile), aCharset); if (string.endsWith(System.getProperty("line.separator"))) { string = string.substring(0, string.length() - 1); } return string; } /** * Reads the contents of a file into a string using the UTF-8 character set encoding. * * @param aFile The file from which to read * @return The information read from the file * @throws IOException If the supplied file could not be read */ public static String read(final File aFile) throws IOException { String string = new String(readBytes(aFile), UTF_8); if (string.endsWith(System.getProperty("line.separator"))) { string = string.substring(0, string.length() - 1); } return string; } /** * Removes empty and null strings from a string array. * * @param aStringArray A varargs that may contain empty or null strings * @return A string array without empty or null strings */ public static String[] trim(final String... aStringArray) { final ArrayList<String> list = new ArrayList<>(); for (final String string : aStringArray) { if (string != null && !string.equals("")) { list.add(string); } } return list.toArray(new String[list.size()]); } /** * Returns true if the supplied string is null, empty, or contains nothing but whitespace. * * @param aString A string to test to see if it is null, empty or contains nothing but whitespace * @return True if the supplied string is empty; else, false */ public static boolean isEmpty(final String aString) { boolean result = true; if (aString != null) { for (int index = 0; index < aString.length(); index++) { if (!Character.isWhitespace(aString.charAt(index))) { result = false; break; } } } return result; } /** * A convenience method for toString(Object[], char) to add varargs support. * * @param aPadChar A padding character * @param aVarargs A varargs into which to insert the padding character * @return A string form of the varargs with padding added */ public static String toString(final char aPadChar, final Object... aVarargs) { return toString(aVarargs, aPadChar); } /** * Concatenates the string representations of a series of objects (by calling their <code>Object.toString()</code> * method). Concatenated strings are separated using the supplied 'padding' character. * * @param aObjArray An array of objects whose <code>toString()</code> representations should be concatenated * @param aPadChar The character used to separate concatenated strings * @return A concatenation of the supplied objects' string representations */ public static String toString(final Object[] aObjArray, final char aPadChar) { if (aObjArray == null || aObjArray.length == 0) { return ""; } if (aObjArray.length == 1) { return aObjArray[0].toString(); } final StringBuilder buffer = new StringBuilder(); for (final Object obj : aObjArray) { buffer.append(obj).append(aPadChar); } return buffer.deleteCharAt(buffer.length() - 1).toString(); } /** * Turns the keys in a map into a character delimited string. The order is only consistent if the map is sorted. * * @param aMap The map from which to pull the keys * @param aSeparator The character separator for the construction of the string * @return A string constructed from the keys in the map */ public static String joinKeys(final Map<String, ?> aMap, final char aSeparator) { if (aMap.isEmpty()) { return ""; } final Iterator<String> iterator = aMap.keySet().iterator(); final StringBuilder buffer = new StringBuilder(); while (iterator.hasNext()) { buffer.append(iterator.next()).append(aSeparator); } final int length = buffer.length() - 1; return buffer.charAt(length) == aSeparator ? buffer.substring(0, length) : buffer.toString(); } /** * Provides a toString() method for maps that have string keys and string array values. The regular map toString() * works fine for string keys and string values but, since a string array doesn't have a toString(), the map's * toString() method doesn't produce a useful output. This fixes that. * * @param aMap A map of string keys and string array values to turn into a single string representation of the map * @return A concatenation of the supplied map's string values */ public static String toString(final Map<String, String[]> aMap) { final Set<Entry<String, String[]>> set = aMap.entrySet(); final Iterator<Entry<String, String[]>> setIter = set.iterator(); final StringBuilder buffer = new StringBuilder(); while (setIter.hasNext()) { final Entry<String, String[]> entry = setIter.next(); final Object[] values = entry.getValue(); buffer.append(entry.getKey()).append('='); for (final Object value : values) { buffer.append('{').append(value).append('}'); } buffer.append('&'); } if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == '&') { buffer.deleteCharAt(buffer.length() - 1); } return buffer.toString(); } /** * Adds line numbers to any string. This is useful when I need to debug an XQuery outside the context of an IDE * (i.e., in debugging output). * * @param aMessage Text to which line numbers should be added * @return The supplied text with line numbers at the first of each line */ public static String addLineNumbers(final String aMessage) { final String eol = System.getProperty("line.separator"); final String[] lines = aMessage.split(eol); final StringBuilder buffer = new StringBuilder(); int lineCount = 1; // Error messages start with line 1 for (final String line : lines) { buffer.append(lineCount++).append(' ').append(line); buffer.append(eol); } final int length = buffer.length(); buffer.delete(length - eol.length(), length); return buffer.toString(); } /** * Reads the contents of a file into a byte array. * * @param aFile The file from which to read * @return The bytes read from the file * @throws IOException If the supplied file could not be read in its entirety */ private static byte[] readBytes(final File aFile) throws IOException { final FileInputStream fileStream = new FileInputStream(aFile); final ByteBuffer buf = ByteBuffer.allocate((int) aFile.length()); final int read = fileStream.getChannel().read(buf); if (read != aFile.length()) { fileStream.close(); throw new IOException("Failed to read whole file"); } fileStream.close(); return buf.array(); } /** * Parses strings with an integer range (e.g., 2-5) and returns an expanded integer array {2, 3, 4, 5} with those * values. * * @param aIntRange A string representation of a range of integers * @return An int array with the expanded values of the string representation */ public static int[] parseIntRange(final String aIntRange) { final String[] range = aIntRange.split("-"); final int[] ints; if (range.length == 1) { ints = new int[range.length]; ints[0] = Integer.parseInt(aIntRange); } else { final int start = Integer.parseInt(range[0]); final int end = Integer.parseInt(range[1]); if (end >= start) { int position = 0; final int size = end - start; ints = new int[size + 1]; // because we're zero-based for (int index = start; index <= end; index++) { ints[position++] = index; } } else { throw new NumberFormatException("Inverted number range: " + start + "-" + end); } } return ints; } /** * Returns a human-friendly, locale dependent, string representation of the supplied int; for instance, "1" becomes * "first", "2" becomes "second", etc. * * @param aInt An int to convert into a string * @return The string form of the supplied int */ public static String toString(final int aInt) { return toUpcaseString(aInt).toLowerCase(Locale.getDefault()); } /** * Reverses the characters in a string. * * @param aString A string whose characters are to be reversed * @return A string with the supplied string reversed */ public static String reverse(final String aString) { return new StringBuffer(aString).reverse().toString(); } /** * Upcases a string. * * @param aString A string to upcase * @return The upcased string */ public static String upcase(final String aString) { return aString.substring(0, 1).toUpperCase() + aString.substring(1); } /** * Returns an up-cased human-friendly string representation for the supplied int; for instance, "1" becomes "First", * "2" becomes "Second", etc. * * @param aInt An int to convert into a string * @return The string form of the supplied int */ public static String toUpcaseString(final int aInt) { switch (aInt) { case 1: return "First"; case 2: return "Second"; case 3: return "Third"; case 4: return "Fourth"; case 5: return "Fifth"; case 6: return "Sixth"; case 7: return "Seventh"; case 8: return "Eighth"; case 9: return "Ninth"; case 10: return "Tenth"; case 11: return "Eleventh"; case 12: return "Twelveth"; case 13: return "Thirteenth"; case 14: return "Fourteenth"; case 15: return "Fifthteenth"; case 16: return "Sixteenth"; case 17: return "Seventeenth"; case 18: return "Eighteenth"; case 19: return "Nineteenth"; case 20: return "Twentieth"; default: throw new UnsupportedOperationException("Don't have a string value for " + aInt); } } }