/* * ModeShape (http://www.modeshape.org) * * 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 org.modeshape.common.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.Array; import java.math.BigInteger; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.modeshape.common.CommonI18n; import org.modeshape.common.annotation.Immutable; /** * Utilities for string processing and manipulation. */ @Immutable public class StringUtil { public static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Pattern NORMALIZE_PATTERN = Pattern.compile("\\s+"); private static final Pattern PARAMETER_COUNT_PATTERN = Pattern.compile("\\{(\\d+)\\}"); /** * Combine the lines into a single string, using the new line character as the delimiter. This is compatible with * {@link #splitLines(String)}. * * @param lines the lines to be combined * @return the combined lines, or an empty string if there are no lines */ public static String combineLines( String[] lines ) { return combineLines(lines, '\n'); } /** * Combine the lines into a single string, using the supplied separator as the delimiter. * * @param lines the lines to be combined * @param separator the separator character * @return the combined lines, or an empty string if there are no lines */ public static String combineLines( String[] lines, char separator ) { if (lines == null || lines.length == 0) return ""; StringBuilder sb = new StringBuilder(); for (int i = 0; i != lines.length; ++i) { String line = lines[i]; if (i != 0) sb.append(separator); sb.append(line); } return sb.toString(); } /** * Split the supplied content into lines, returning each line as an element in the returned list. * * @param content the string content that is to be split * @return the list of lines; never null but may be an empty (unmodifiable) list if the supplied content is null or empty */ public static List<String> splitLines( final String content ) { if (content == null || content.length() == 0) return Collections.emptyList(); String[] lines = content.split("[\\r]?\\n"); return Arrays.asList(lines); } /** * Combine the supplied values into a single string, using the supplied string to delimit values. * * @param values the values to be combined; may not be null, but may contain null values (which are skipped) * @param delimiter the characters to place between each of the values; may not be null * @return the joined string; never null * @see String#split(String) */ public static String join( Object[] values, String delimiter ) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Object segment : values) { if (segment == null) continue; if (first) first = false; else sb.append(delimiter); sb.append(segment); } return sb.toString(); } /** * Combine the supplied values into a single string, using the supplied string to delimit values. * * @param values the values to be combined; may not be null, but may contain null values (which are skipped) * @param delimiter the characters to place between each of the values; may not be null * @return the joined string; never null * @see String#split(String) */ public static String join( Iterable<?> values, String delimiter ) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Object segment : values) { if (segment == null) continue; if (first) first = false; else sb.append(delimiter); sb.append(segment); } return sb.toString(); } /** * Create a string by substituting the parameters into all key occurrences in the supplied format. The pattern consists of * zero or more keys of the form <code>{n}</code>, where <code>n</code> is an integer starting at 0. Therefore, the first * parameter replaces all occurrences of "{0}", the second parameter replaces all occurrences of "{1}", etc. * <p> * If any parameter is null, the corresponding key is replaced with the string "null". Therefore, consider using an empty * string when keys are to be removed altogether. * </p> * <p> * If there are no parameters, this method does nothing and returns the supplied pattern as is. * </p> * * @param pattern the pattern * @param parameters the parameters used to replace keys * @return the string with all keys replaced (or removed) */ public static String createString( String pattern, Object... parameters ) { CheckArg.isNotNull(pattern, "pattern"); if (parameters == null) parameters = EMPTY_STRING_ARRAY; Matcher matcher = PARAMETER_COUNT_PATTERN.matcher(pattern); // CHECKSTYLE IGNORE check FOR NEXT 1 LINES StringBuffer text = new StringBuffer(); int requiredParameterCount = 0; boolean err = false; while (matcher.find()) { int ndx = Integer.valueOf(matcher.group(1)); if (requiredParameterCount <= ndx) { requiredParameterCount = ndx + 1; } if (ndx >= parameters.length) { err = true; matcher.appendReplacement(text, matcher.group()); } else { Object parameter = parameters[ndx]; // Automatically pretty-print arrays if (parameter != null && parameter.getClass().isArray()) { if (parameter instanceof Object[]) { parameter = Arrays.asList((Object[])parameter); } else { int length = Array.getLength(parameter); List<Object> parameterAsList = new ArrayList<Object>(length); for (int i = 0; i < length; i++) { parameterAsList.add(Array.get(parameter, i)); } parameter = parameterAsList; } } matcher.appendReplacement(text, Matcher.quoteReplacement(parameter == null ? "null" : parameter.toString())); } } if (err || requiredParameterCount < parameters.length) { throw new IllegalArgumentException( CommonI18n.requiredToSuppliedParameterMismatch.text(parameters.length, parameters.length == 1 ? "" : "s", requiredParameterCount, requiredParameterCount == 1 ? "" : "s", pattern, text.toString())); } matcher.appendTail(text); return text.toString(); } /** * Create a new string containing the specified character repeated a specific number of times. * * @param charToRepeat the character to repeat * @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0 * @return the resulting string */ public static String createString( final char charToRepeat, int numberOfRepeats ) { assert numberOfRepeats >= 0; StringBuilder sb = new StringBuilder(); for (int i = 0; i < numberOfRepeats; ++i) { sb.append(charToRepeat); } return sb.toString(); } /** * Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or * truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not * remove leading and trailing whitespace. * * @param original the string for which the length is to be set; may not be null * @param length the desired length; must be positive * @param padChar the character to use for padding, if the supplied string is not long enough * @return the string of the desired length * @see #justifyLeft(String, int, char) */ public static String setLength( String original, int length, char padChar ) { return justifyLeft(original, length, padChar, false); } public static enum Justify { LEFT, RIGHT, CENTER; } /** * Justify the contents of the string. * * @param justify the way in which the string is to be justified * @param str the string to be right justified; if null, an empty string is used * @param width the desired width of the string; must be positive * @param padWithChar the character to use for padding, if needed * @return the right justified string */ public static String justify( Justify justify, String str, final int width, char padWithChar ) { switch (justify) { case LEFT: return justifyLeft(str, width, padWithChar); case RIGHT: return justifyRight(str, width, padWithChar); case CENTER: return justifyCenter(str, width, padWithChar); } assert false; return null; } /** * Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is * longer than the desired width, the leading characters are removed so that the last character in the supplied string at the * last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more * times such that the last character in the supplied string appears as the last character in the resulting string and that * the length matches that specified. * * @param str the string to be right justified; if null, an empty string is used * @param width the desired width of the string; must be positive * @param padWithChar the character to use for padding, if needed * @return the right justified string */ public static String justifyRight( String str, final int width, char padWithChar ) { assert width > 0; // Trim the leading and trailing whitespace ... str = str != null ? str.trim() : ""; final int length = str.length(); int addChars = width - length; if (addChars < 0) { // truncate the first characters, keep the last return str.subSequence(length - width, length).toString(); } // Prepend the whitespace ... final StringBuilder sb = new StringBuilder(); while (addChars > 0) { sb.append(padWithChar); --addChars; } // Write the content ... sb.append(str); return sb.toString(); } /** * Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the * resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the * specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of * the string one or more times such that the length is that specified. All leading and trailing whitespace is removed. * * @param str the string to be left justified; if null, an empty string is used * @param width the desired width of the string; must be positive * @param padWithChar the character to use for padding, if needed * @return the left justified string * @see #setLength(String, int, char) */ public static String justifyLeft( String str, final int width, char padWithChar ) { return justifyLeft(str, width, padWithChar, true); } protected static String justifyLeft( String str, final int width, char padWithChar, boolean trimWhitespace ) { // Trim the leading and trailing whitespace ... str = str != null ? (trimWhitespace ? str.trim() : str) : ""; int addChars = width - str.length(); if (addChars < 0) { // truncate return str.subSequence(0, width).toString(); } // Write the content ... final StringBuilder sb = new StringBuilder(); sb.append(str); // Append the whitespace ... while (addChars > 0) { sb.append(padWithChar); --addChars; } return sb.toString(); } /** * Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the * specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning * and end of the string such that the length is that specified; one additional padding character is prepended if required. * All leading and trailing whitespace is removed before centering. * * @param str the string to be left justified; if null, an empty string is used * @param width the desired width of the string; must be positive * @param padWithChar the character to use for padding, if needed * @return the left justified string * @see #setLength(String, int, char) */ public static String justifyCenter( String str, final int width, char padWithChar ) { // Trim the leading and trailing whitespace ... str = str != null ? str.trim() : ""; int addChars = width - str.length(); if (addChars < 0) { // truncate return str.subSequence(0, width).toString(); } // Write the content ... int prependNumber = addChars / 2; int appendNumber = prependNumber; if ((prependNumber + appendNumber) != addChars) { ++prependNumber; } final StringBuilder sb = new StringBuilder(); // Prepend the pad character(s) ... while (prependNumber > 0) { sb.append(padWithChar); --prependNumber; } // Add the actual content sb.append(str); // Append the pad character(s) ... while (appendNumber > 0) { sb.append(padWithChar); --appendNumber; } return sb.toString(); } /** * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied * object is null. * * @param obj the object from which the string is to be obtained using {@link Object#toString()}. * @param maxLength the maximum length of the string being returned * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the * maximum length (including the suffix) * @throws IllegalArgumentException if the maximum length is negative */ public static String truncate( Object obj, int maxLength ) { return truncate(obj, maxLength, null); } /** * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied * object is null. * * @param obj the object from which the string is to be obtained using {@link Object#toString()}. * @param maxLength the maximum length of the string being returned * @param suffix the suffix that should be added to the content if the string must be truncated, or null if the default suffix * of "..." should be used * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the * maximum length (including the suffix) * @throws IllegalArgumentException if the maximum length is negative */ public static String truncate( Object obj, int maxLength, String suffix ) { CheckArg.isNonNegative(maxLength, "maxLength"); if (obj == null || maxLength == 0) { return ""; } String str = obj.toString(); if (str.length() <= maxLength) return str; if (suffix == null) suffix = "..."; int maxNumChars = maxLength - suffix.length(); if (maxNumChars < 0) { // Then the max length is actually shorter than the suffix ... str = suffix.substring(0, maxLength); } else if (str.length() > maxNumChars) { str = str.substring(0, maxNumChars) + suffix; } return str; } /** * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished * reading. * * @param reader the reader of the contents; may be null * @return the contents, or an empty string if the supplied reader is null * @throws IOException if there is an error reading the content */ public static String read( Reader reader ) throws IOException { return IoUtil.read(reader); } /** * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished * reading. * * @param stream the streamed contents; may be null * @return the contents, or an empty string if the supplied stream is null * @throws IOException if there is an error reading the content */ public static String read( InputStream stream ) throws IOException { return IoUtil.read(stream); } /** * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when * finished. * * @param content the content to write to the stream; may be null * @param stream the stream to which the content is to be written * @throws IOException * @throws IllegalArgumentException if the stream is null */ public static void write( String content, OutputStream stream ) throws IOException { IoUtil.write(content, stream); } /** * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when * finished. * * @param content the content to write to the writer; may be null * @param writer the writer to which the content is to be written * @throws IOException * @throws IllegalArgumentException if the writer is null */ public static void write( String content, Writer writer ) throws IOException { IoUtil.write(content, writer); } /** * Get the stack trace of the supplied exception. * * @param throwable the exception for which the stack trace is to be returned * @return the stack trace, or null if the supplied exception is null */ public static String getStackTrace( Throwable throwable ) { if (throwable == null) return null; final ByteArrayOutputStream bas = new ByteArrayOutputStream(); final PrintWriter pw = new PrintWriter(bas); throwable.printStackTrace(pw); pw.close(); return bas.toString(); } /** * Removes leading and trailing whitespace from the supplied text, and reduces other consecutive whitespace characters to a * single space. Whitespace includes line-feeds. * * @param text the text to be normalized * @return the normalized text */ public static String normalize( String text ) { CheckArg.isNotNull(text, "text"); // This could be much more efficient. return NORMALIZE_PATTERN.matcher(text).replaceAll(" ").trim(); } private static final byte[] HEX_CHAR_TABLE = {(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'}; /** * Get the hexadecimal string representation of the supplied byte array. * * @param bytes the byte array * @return the hex string representation of the byte array; never null */ public static String getHexString( byte[] bytes ) { try { byte[] hex = new byte[2 * bytes.length]; int index = 0; for (byte b : bytes) { int v = b & 0xFF; hex[index++] = HEX_CHAR_TABLE[v >>> 4]; hex[index++] = HEX_CHAR_TABLE[v & 0xF]; } return new String(hex, "ASCII"); } catch (UnsupportedEncodingException e) { BigInteger bi = new BigInteger(1, bytes); return String.format("%0" + (bytes.length << 1) + "x", bi); } } public static byte[] fromHexString( String hexadecimal ) { int len = hexadecimal.length(); if (len % 2 != 0) { hexadecimal = "0" + hexadecimal; } byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte)((Character.digit(hexadecimal.charAt(i), 16) << 4) + Character.digit(hexadecimal.charAt(i + 1), 16)); } return data; } public static boolean isHexString( String hexadecimal ) { int len = hexadecimal.length(); for (int i = 0; i < len; ++i) { if (!isHexCharacter(hexadecimal.charAt(i))) return false; } return true; } public static boolean isHexCharacter( char c ) { return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); } /** * Returns true if the given string is null or represents the empty string * * @param str the string; may be null or empty * @return true if the string is null or contains only whitespace */ public static boolean isBlank( String str ) { return str == null || str.trim().isEmpty(); } /** * Returns true if the given string is not null and does not represents the empty string * * @param str the string; may be null or empty * @return true if the string is not null and not empty, false otherwise */ public static boolean notBlank( String str ) { return !isBlank(str); } /** * Return whether the supplied string contains any of the supplied characters. * * @param str the string to be examined; may not be null * @param chars the characters to be found within the supplied string; may be zero-length * @return true if the supplied string contains at least one of the supplied characters, or false otherwise */ public static boolean containsAnyOf( String str, char... chars ) { CharacterIterator iter = new StringCharacterIterator(str); for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { for (char match : chars) { if (c == match) return true; } } return false; } private StringUtil() { // Prevent construction } }