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 =&gt; [ &quot;description&quot;, 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    }