001 /* 002 * JBoss, Home of Professional Open Source. 003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors 004 * as indicated by the @author tags. See the copyright.txt file in the 005 * distribution for a full listing of individual contributors. 006 * 007 * This is free software; you can redistribute it and/or modify it 008 * under the terms of the GNU Lesser General Public License as 009 * published by the Free Software Foundation; either version 2.1 of 010 * the License, or (at your option) any later version. 011 * 012 * This software is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this software; if not, write to the Free 019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 021 */ 022 package org.jboss.dna.common.util; 023 024 import java.io.ByteArrayOutputStream; 025 import java.io.IOException; 026 import java.io.InputStream; 027 import java.io.OutputStream; 028 import java.io.PrintWriter; 029 import java.io.Reader; 030 import java.io.UnsupportedEncodingException; 031 import java.io.Writer; 032 import java.util.Arrays; 033 import java.util.Calendar; 034 import java.util.Collection; 035 import java.util.Collections; 036 import java.util.List; 037 import java.util.Map; 038 import java.util.regex.Matcher; 039 import java.util.regex.Pattern; 040 import org.jboss.dna.common.CommonI18n; 041 import org.jboss.dna.common.SystemFailureException; 042 043 /** 044 * Utilities for string processing and manipulation. 045 */ 046 public class StringUtil { 047 048 public static final String[] EMPTY_STRING_ARRAY = new String[0]; 049 private static final Pattern NORMALIZE_PATTERN = Pattern.compile("\\s+"); 050 private static final Pattern PARAMETER_COUNT_PATTERN = Pattern.compile("\\{(\\d+)\\}"); 051 052 /** 053 * Split the supplied content into lines, returning each line as an element in the returned list. 054 * 055 * @param content the string content that is to be split 056 * @return the list of lines; never null but may be an empty (unmodifiable) list if the supplied content is null or empty 057 */ 058 public static List<String> splitLines( final String content ) { 059 if (content == null || content.length() == 0) return Collections.emptyList(); 060 String[] lines = content.split("[\\r]?\\n"); 061 return Arrays.asList(lines); 062 } 063 064 /** 065 * Create a string by substituting the parameters into all key occurrences in the supplied format. The pattern consists of 066 * zero or more keys of the form <code>{n}</code>, where <code>n</code> is an integer starting at 1. Therefore, the first 067 * parameter replaces all occurrences of "{1}", the second parameter replaces all occurrences of "{2}", etc. 068 * <p> 069 * If any parameter is null, the corresponding key is replaced with the string "null". Therefore, consider using an empty 070 * string when keys are to be removed altogether. 071 * </p> 072 * <p> 073 * If there are no parameters, this method does nothing and returns the supplied pattern as is. 074 * </p> 075 * 076 * @param pattern the pattern 077 * @param parameters the parameters used to replace keys 078 * @return the string with all keys replaced (or removed) 079 */ 080 public static String createString( String pattern, 081 Object... parameters ) { 082 CheckArg.isNotNull(pattern, "pattern"); 083 if (parameters == null) parameters = EMPTY_STRING_ARRAY; 084 Matcher matcher = PARAMETER_COUNT_PATTERN.matcher(pattern); 085 StringBuffer text = new StringBuffer(); 086 int requiredParameterCount = 0; 087 boolean err = false; 088 while (matcher.find()) { 089 int ndx = Integer.valueOf(matcher.group(1)); 090 if (requiredParameterCount <= ndx) { 091 requiredParameterCount = ndx + 1; 092 } 093 if (ndx >= parameters.length) { 094 err = true; 095 matcher.appendReplacement(text, matcher.group()); 096 } else { 097 Object parameter = parameters[ndx]; 098 matcher.appendReplacement(text, Matcher.quoteReplacement(parameter == null ? "null" : parameter.toString())); 099 } 100 } 101 if (err || requiredParameterCount < parameters.length) { 102 throw new IllegalArgumentException( 103 CommonI18n.requiredToSuppliedParameterMismatch.text(parameters.length, 104 parameters.length == 1 ? "" : "s", 105 requiredParameterCount, 106 requiredParameterCount == 1 ? "" : "s", 107 pattern, 108 text.toString())); 109 } 110 matcher.appendTail(text); 111 112 return text.toString(); 113 } 114 115 /** 116 * Create a new string containing the specified character repeated a specific number of times. 117 * 118 * @param charToRepeat the character to repeat 119 * @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0 120 * @return the resulting string 121 */ 122 public static String createString( final char charToRepeat, 123 int numberOfRepeats ) { 124 assert numberOfRepeats >= 0; 125 StringBuilder sb = new StringBuilder(); 126 for (int i = 0; i < numberOfRepeats; ++i) { 127 sb.append(charToRepeat); 128 } 129 return sb.toString(); 130 } 131 132 /** 133 * Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or 134 * truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not 135 * remove leading and trailing whitespace. 136 * 137 * @param original the string for which the length is to be set; may not be null 138 * @param length the desired length; must be positive 139 * @param padChar the character to use for padding, if the supplied string is not long enough 140 * @return the string of the desired length 141 * @see #justifyLeft(String, int, char) 142 */ 143 public static String setLength( String original, 144 int length, 145 char padChar ) { 146 return justifyLeft(original, length, padChar, false); 147 } 148 149 /** 150 * Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is 151 * longer than the desired width, the leading characters are removed so that the last character in the supplied string at the 152 * last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more 153 * times such that the last character in the supplied string appears as the last character in the resulting string and that 154 * the length matches that specified. 155 * 156 * @param str the string to be right justified; if null, an empty string is used 157 * @param width the desired width of the string; must be positive 158 * @param padWithChar the character to use for padding, if needed 159 * @return the right justified string 160 */ 161 public static String justifyRight( String str, 162 final int width, 163 char padWithChar ) { 164 assert width > 0; 165 // Trim the leading and trailing whitespace ... 166 str = str != null ? str.trim() : ""; 167 168 final int length = str.length(); 169 int addChars = width - length; 170 if (addChars < 0) { 171 // truncate the first characters, keep the last 172 return str.subSequence(length - width, length).toString(); 173 } 174 // Prepend the whitespace ... 175 final StringBuilder sb = new StringBuilder(); 176 while (addChars > 0) { 177 sb.append(padWithChar); 178 --addChars; 179 } 180 181 // Write the content ... 182 sb.append(str); 183 return sb.toString(); 184 } 185 186 /** 187 * Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the 188 * resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the 189 * specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of 190 * the string one or more times such that the length is that specified. All leading and trailing whitespace is removed. 191 * 192 * @param str the string to be left justified; if null, an empty string is used 193 * @param width the desired width of the string; must be positive 194 * @param padWithChar the character to use for padding, if needed 195 * @return the left justified string 196 * @see #setLength(String, int, char) 197 */ 198 public static String justifyLeft( String str, 199 final int width, 200 char padWithChar ) { 201 return justifyLeft(str, width, padWithChar, true); 202 } 203 204 protected static String justifyLeft( String str, 205 final int width, 206 char padWithChar, 207 boolean trimWhitespace ) { 208 // Trim the leading and trailing whitespace ... 209 str = str != null ? (trimWhitespace ? str.trim() : str) : ""; 210 211 int addChars = width - str.length(); 212 if (addChars < 0) { 213 // truncate 214 return str.subSequence(0, width).toString(); 215 } 216 // Write the content ... 217 final StringBuilder sb = new StringBuilder(); 218 sb.append(str); 219 220 // Append the whitespace ... 221 while (addChars > 0) { 222 sb.append(padWithChar); 223 --addChars; 224 } 225 226 return sb.toString(); 227 } 228 229 /** 230 * Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the 231 * specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning 232 * and end of the string such that the length is that specified; one additional padding character is prepended if required. 233 * All leading and trailing whitespace is removed before centering. 234 * 235 * @param str the string to be left justified; if null, an empty string is used 236 * @param width the desired width of the string; must be positive 237 * @param padWithChar the character to use for padding, if needed 238 * @return the left justified string 239 * @see #setLength(String, int, char) 240 */ 241 public static String justifyCenter( String str, 242 final int width, 243 char padWithChar ) { 244 // Trim the leading and trailing whitespace ... 245 str = str != null ? str.trim() : ""; 246 247 int addChars = width - str.length(); 248 if (addChars < 0) { 249 // truncate 250 return str.subSequence(0, width).toString(); 251 } 252 // Write the content ... 253 int prependNumber = addChars / 2; 254 int appendNumber = prependNumber; 255 if ((prependNumber + appendNumber) != addChars) { 256 ++prependNumber; 257 } 258 259 final StringBuilder sb = new StringBuilder(); 260 261 // Prepend the pad character(s) ... 262 while (prependNumber > 0) { 263 sb.append(padWithChar); 264 --prependNumber; 265 } 266 267 // Add the actual content 268 sb.append(str); 269 270 // Append the pad character(s) ... 271 while (appendNumber > 0) { 272 sb.append(padWithChar); 273 --appendNumber; 274 } 275 276 return sb.toString(); 277 } 278 279 /** 280 * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied 281 * object is null. 282 * 283 * @param obj the object from which the string is to be obtained using {@link Object#toString()}. 284 * @param maxLength the maximum length of the string being returned 285 * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the 286 * maximum length (including the suffix) 287 * @throws IllegalArgumentException if the maximum length is negative 288 */ 289 public static String truncate( Object obj, 290 int maxLength ) { 291 return truncate(obj, maxLength, null); 292 } 293 294 /** 295 * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied 296 * object is null. 297 * 298 * @param obj the object from which the string is to be obtained using {@link Object#toString()}. 299 * @param maxLength the maximum length of the string being returned 300 * @param suffix the suffix that should be added to the content if the string must be truncated, or null if the default suffix 301 * of "..." should be used 302 * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the 303 * maximum length (including the suffix) 304 * @throws IllegalArgumentException if the maximum length is negative 305 */ 306 public static String truncate( Object obj, 307 int maxLength, 308 String suffix ) { 309 CheckArg.isNonNegative(maxLength, "maxLength"); 310 if (obj == null || maxLength == 0) { 311 return ""; 312 } 313 String str = obj.toString(); 314 if (str.length() <= maxLength) return str; 315 if (suffix == null) suffix = "..."; 316 int maxNumChars = maxLength - suffix.length(); 317 if (maxNumChars < 0) { 318 // Then the max length is actually shorter than the suffix ... 319 str = suffix.substring(0, maxLength); 320 } else if (str.length() > maxNumChars) { 321 str = str.substring(0, maxNumChars) + suffix; 322 } 323 return str; 324 } 325 326 /** 327 * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished 328 * reading. 329 * 330 * @param reader the reader of the contents; may be null 331 * @return the contents, or an empty string if the supplied reader is null 332 * @throws IOException if there is an error reading the content 333 */ 334 public static String read( Reader reader ) throws IOException { 335 return IoUtil.read(reader); 336 } 337 338 /** 339 * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished 340 * reading. 341 * 342 * @param stream the streamed contents; may be null 343 * @return the contents, or an empty string if the supplied stream is null 344 * @throws IOException if there is an error reading the content 345 */ 346 public static String read( InputStream stream ) throws IOException { 347 return IoUtil.read(stream); 348 } 349 350 /** 351 * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when 352 * finished. 353 * 354 * @param content the content to write to the stream; may be null 355 * @param stream the stream to which the content is to be written 356 * @throws IOException 357 * @throws IllegalArgumentException if the stream is null 358 */ 359 public static void write( String content, 360 OutputStream stream ) throws IOException { 361 IoUtil.write(content, stream); 362 } 363 364 /** 365 * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when 366 * finished. 367 * 368 * @param content the content to write to the writer; may be null 369 * @param writer the writer to which the content is to be written 370 * @throws IOException 371 * @throws IllegalArgumentException if the writer is null 372 */ 373 public static void write( String content, 374 Writer writer ) throws IOException { 375 IoUtil.write(content, writer); 376 } 377 378 /** 379 * Create a human-readable form of the supplied object by choosing the representation format based upon the object type. 380 * <p> 381 * <ul> 382 * <li>A null reference results in the "null" string.</li> 383 * <li>A string is written wrapped by double quotes.</li> 384 * <li>A boolean is written using {@link Boolean#toString()}.</li> 385 * <li>A {@link Number number} is written using the standard {@link Number#toString() toString()} method.</li> 386 * <li>A {@link java.util.Date date} is written using the the {@link DateUtil#getDateAsStandardString(java.util.Date)} utility 387 * method.</li> 388 * <li>A {@link java.sql.Date SQL date} is written using the the {@link DateUtil#getDateAsStandardString(java.util.Date)} 389 * utility method.</li> 390 * <li>A {@link Calendar Calendar instance} is written using the the {@link DateUtil#getDateAsStandardString(Calendar)} 391 * utility method.</li> 392 * <li>An array of bytes is written with a leading "[ " and trailing " ]" surrounding the bytes written as UTF-8. 393 * <li>An array of objects is written with a leading "[ " and trailing " ]", and with all objects sent through 394 * {@link #readableString(Object)} and separated by ", ".</li> 395 * <li>A collection of objects (e.g, <code>Collection<?></code>) is written with a leading "[ " and trailing " ]", and with 396 * all objects sent through {@link #readableString(Object)} and separated by ", ".</li> 397 * <li>A map of objects (e.g, <code>Map<?></code>) is written with a leading "{ " and trailing " }", and with all map entries 398 * written in the form "key => value" and separated by ", ". All key and value objects are sent through the 399 * {@link #readableString(Object)} method.</li> 400 * <li>Any other object is written using the object's {@link Object#toString() toString()} method.</li> 401 * </ul> 402 * </p> 403 * <p> 404 * This method is capable of generating strings for nested objects. For example, a <code>Map<Date,Object[]></code> would be 405 * written in the form: 406 * 407 * <pre> 408 * { 2008-02-03T14:22:49 => [ "description", 3, [ 003459de7389g23aef, true ] ] } 409 * </pre> 410 * 411 * </p> 412 * 413 * @param obj the object that is to be converted to a string. 414 * @return the string representation that is to be human readable 415 */ 416 public static String readableString( Object obj ) { 417 if (obj == null) return "null"; 418 if (obj instanceof Boolean) return ((Boolean)obj).toString(); 419 if (obj instanceof String) return "\"" + obj.toString() + "\""; 420 if (obj instanceof Number) return obj.toString(); 421 if (obj instanceof Map<?, ?>) return readableString((Map<?, ?>)obj); 422 if (obj instanceof Collection<?>) return readableString((Collection<?>)obj); 423 if (obj instanceof byte[]) return readableString((byte[])obj); 424 if (obj instanceof boolean[]) return readableString((boolean[])obj); 425 if (obj instanceof short[]) return readableString((short[])obj); 426 if (obj instanceof int[]) return readableString((int[])obj); 427 if (obj instanceof long[]) return readableString((long[])obj); 428 if (obj instanceof float[]) return readableString((float[])obj); 429 if (obj instanceof double[]) return readableString((double[])obj); 430 if (obj instanceof Object[]) return readableString((Object[])obj); 431 if (obj instanceof Calendar) return DateUtil.getDateAsStandardString((Calendar)obj); 432 if (obj instanceof java.util.Date) return DateUtil.getDateAsStandardString((java.util.Date)obj); 433 if (obj instanceof java.sql.Date) return DateUtil.getDateAsStandardString((java.sql.Date)obj); 434 return obj.toString(); 435 } 436 437 protected static String readableEmptyArray( Class<?> arrayClass ) { 438 assert arrayClass != null; 439 Class<?> componentType = arrayClass.getComponentType(); 440 if (componentType.isArray()) return "[" + readableEmptyArray(componentType) + "]"; 441 return "[]"; 442 } 443 444 protected static String readableString( Object[] array ) { 445 assert array != null; 446 if (array.length == 0) return readableEmptyArray(array.getClass()); 447 StringBuilder sb = new StringBuilder(); 448 boolean first = true; 449 sb.append("[ "); 450 for (Object value : array) { 451 if (first) { 452 first = false; 453 } else { 454 sb.append(", "); 455 } 456 sb.append(readableString(value)); 457 } 458 sb.append(" ]"); 459 return sb.toString(); 460 } 461 462 protected static String readableString( int[] array ) { 463 assert array != null; 464 if (array.length == 0) return readableEmptyArray(array.getClass()); 465 StringBuilder sb = new StringBuilder(); 466 boolean first = true; 467 sb.append("[ "); 468 for (int value : array) { 469 if (first) { 470 first = false; 471 } else { 472 sb.append(", "); 473 } 474 sb.append(readableString(value)); 475 } 476 sb.append(" ]"); 477 return sb.toString(); 478 } 479 480 protected static String readableString( short[] array ) { 481 assert array != null; 482 if (array.length == 0) return readableEmptyArray(array.getClass()); 483 StringBuilder sb = new StringBuilder(); 484 boolean first = true; 485 sb.append("[ "); 486 for (short value : array) { 487 if (first) { 488 first = false; 489 } else { 490 sb.append(", "); 491 } 492 sb.append(readableString(value)); 493 } 494 sb.append(" ]"); 495 return sb.toString(); 496 } 497 498 protected static String readableString( long[] array ) { 499 assert array != null; 500 if (array.length == 0) return readableEmptyArray(array.getClass()); 501 StringBuilder sb = new StringBuilder(); 502 boolean first = true; 503 sb.append("[ "); 504 for (long value : array) { 505 if (first) { 506 first = false; 507 } else { 508 sb.append(", "); 509 } 510 sb.append(readableString(value)); 511 } 512 sb.append(" ]"); 513 return sb.toString(); 514 } 515 516 protected static String readableString( boolean[] array ) { 517 assert array != null; 518 if (array.length == 0) return readableEmptyArray(array.getClass()); 519 StringBuilder sb = new StringBuilder(); 520 boolean first = true; 521 sb.append("[ "); 522 for (boolean value : array) { 523 if (first) { 524 first = false; 525 } else { 526 sb.append(", "); 527 } 528 sb.append(readableString(value)); 529 } 530 sb.append(" ]"); 531 return sb.toString(); 532 } 533 534 protected static String readableString( float[] array ) { 535 assert array != null; 536 if (array.length == 0) return readableEmptyArray(array.getClass()); 537 StringBuilder sb = new StringBuilder(); 538 boolean first = true; 539 sb.append("[ "); 540 for (float value : array) { 541 if (first) { 542 first = false; 543 } else { 544 sb.append(", "); 545 } 546 sb.append(readableString(value)); 547 } 548 sb.append(" ]"); 549 return sb.toString(); 550 } 551 552 protected static String readableString( double[] array ) { 553 assert array != null; 554 if (array.length == 0) return readableEmptyArray(array.getClass()); 555 StringBuilder sb = new StringBuilder(); 556 boolean first = true; 557 sb.append("[ "); 558 for (double value : array) { 559 if (first) { 560 first = false; 561 } else { 562 sb.append(", "); 563 } 564 sb.append(readableString(value)); 565 } 566 sb.append(" ]"); 567 return sb.toString(); 568 } 569 570 protected static String readableString( byte[] array ) { 571 assert array != null; 572 if (array.length == 0) return readableEmptyArray(array.getClass()); 573 StringBuilder sb = new StringBuilder(); 574 sb.append("[ "); 575 try { 576 sb.append(new String(array, "UTF-8")); 577 } catch (UnsupportedEncodingException e) { 578 throw new SystemFailureException(e); 579 } 580 sb.append(" ]"); 581 return sb.toString(); 582 } 583 584 protected static String readableString( Collection<?> collection ) { 585 assert collection != null; 586 if (collection.isEmpty()) return "[]"; 587 StringBuilder sb = new StringBuilder(); 588 boolean first = true; 589 sb.append("[ "); 590 for (Object value : collection) { 591 if (first) { 592 first = false; 593 } else { 594 sb.append(", "); 595 } 596 sb.append(readableString(value)); 597 } 598 sb.append(" ]"); 599 return sb.toString(); 600 } 601 602 protected static String readableString( Map<?, ?> map ) { 603 assert map != null; 604 if (map.isEmpty()) return "{}"; 605 StringBuilder sb = new StringBuilder(); 606 boolean first = true; 607 sb.append("{ "); 608 for (Map.Entry<?, ?> entry : map.entrySet()) { 609 Object key = entry.getKey(); 610 Object value = entry.getValue(); 611 if (first) { 612 first = false; 613 } else { 614 sb.append(", "); 615 } 616 sb.append(readableString(key)); 617 sb.append(" => "); 618 sb.append(readableString(value)); 619 } 620 sb.append(" }"); 621 return sb.toString(); 622 } 623 624 /** 625 * Get the stack trace of the supplied exception. 626 * 627 * @param throwable the exception for which the stack trace is to be returned 628 * @return the stack trace, or null if the supplied exception is null 629 */ 630 public static String getStackTrace( Throwable throwable ) { 631 if (throwable == null) return null; 632 final ByteArrayOutputStream bas = new ByteArrayOutputStream(); 633 final PrintWriter pw = new PrintWriter(bas); 634 throwable.printStackTrace(pw); 635 pw.close(); 636 return bas.toString(); 637 } 638 639 /** 640 * Removes leading and trailing whitespace from the supplied text, and reduces other consecutive whitespace characters to a 641 * single space. Whitespace includes line-feeds. 642 * 643 * @param text the text to be normalized 644 * @return the normalized text 645 */ 646 public static String normalize( String text ) { 647 CheckArg.isNotNull(text, "text"); 648 // This could be much more efficient. 649 return NORMALIZE_PATTERN.matcher(text).replaceAll(" ").trim(); 650 } 651 652 private static final byte[] HEX_CHAR_TABLE = {(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', 653 (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'}; 654 655 /** 656 * Get the hexadecimal string representation of the supplied byte array. 657 * 658 * @param bytes the byte array 659 * @return the hex string representation of the byte array; never null 660 * @throws UnsupportedEncodingException 661 */ 662 public static String getHexString( byte[] bytes ) throws UnsupportedEncodingException { 663 byte[] hex = new byte[2 * bytes.length]; 664 int index = 0; 665 666 for (byte b : bytes) { 667 int v = b & 0xFF; 668 hex[index++] = HEX_CHAR_TABLE[v >>> 4]; 669 hex[index++] = HEX_CHAR_TABLE[v & 0xF]; 670 } 671 return new String(hex, "ASCII"); 672 } 673 674 private StringUtil() { 675 // Prevent construction 676 } 677 }