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 }