001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006    * See the AUTHORS.txt file in the distribution for a full listing of 
007    * individual contributors.
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.common.util;
025    
026    import java.io.ByteArrayOutputStream;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.io.OutputStream;
030    import java.io.PrintWriter;
031    import java.io.Reader;
032    import java.io.UnsupportedEncodingException;
033    import java.io.Writer;
034    import java.util.Arrays;
035    import java.util.Collections;
036    import java.util.List;
037    import java.util.regex.Matcher;
038    import java.util.regex.Pattern;
039    import org.jboss.dna.common.CommonI18n;
040    
041    /**
042     * Utilities for string processing and manipulation.
043     */
044    public class StringUtil {
045    
046        public static final String[] EMPTY_STRING_ARRAY = new String[0];
047        private static final Pattern NORMALIZE_PATTERN = Pattern.compile("\\s+");
048        private static final Pattern PARAMETER_COUNT_PATTERN = Pattern.compile("\\{(\\d+)\\}");
049    
050        /**
051         * Combine the lines into a single string, using the new line character as the delimiter. This is compatible with
052         * {@link #splitLines(String)}.
053         * 
054         * @param lines the lines to be combined
055         * @return the combined lines, or an empty string if there are no lines
056         */
057        public static String combineLines( String[] lines ) {
058            return combineLines(lines, '\n');
059        }
060    
061        /**
062         * Combine the lines into a single string, using the supplied separator as the delimiter.
063         * 
064         * @param lines the lines to be combined
065         * @param separator the separator character
066         * @return the combined lines, or an empty string if there are no lines
067         */
068        public static String combineLines( String[] lines,
069                                           char separator ) {
070            if (lines == null || lines.length == 0) return "";
071            StringBuilder sb = new StringBuilder();
072            for (int i = 0; i != lines.length; ++i) {
073                String line = lines[i];
074                if (i != 0) sb.append(separator);
075                sb.append(line);
076            }
077            return sb.toString();
078        }
079    
080        /**
081         * Split the supplied content into lines, returning each line as an element in the returned list.
082         * 
083         * @param content the string content that is to be split
084         * @return the list of lines; never null but may be an empty (unmodifiable) list if the supplied content is null or empty
085         */
086        public static List<String> splitLines( final String content ) {
087            if (content == null || content.length() == 0) return Collections.emptyList();
088            String[] lines = content.split("[\\r]?\\n");
089            return Arrays.asList(lines);
090        }
091    
092        /**
093         * Create a string by substituting the parameters into all key occurrences in the supplied format. The pattern consists of
094         * zero or more keys of the form <code>{n}</code>, where <code>n</code> is an integer starting at 1. Therefore, the first
095         * parameter replaces all occurrences of "{1}", the second parameter replaces all occurrences of "{2}", etc.
096         * <p>
097         * If any parameter is null, the corresponding key is replaced with the string "null". Therefore, consider using an empty
098         * string when keys are to be removed altogether.
099         * </p>
100         * <p>
101         * If there are no parameters, this method does nothing and returns the supplied pattern as is.
102         * </p>
103         * 
104         * @param pattern the pattern
105         * @param parameters the parameters used to replace keys
106         * @return the string with all keys replaced (or removed)
107         */
108        public static String createString( String pattern,
109                                           Object... parameters ) {
110            CheckArg.isNotNull(pattern, "pattern");
111            if (parameters == null) parameters = EMPTY_STRING_ARRAY;
112            Matcher matcher = PARAMETER_COUNT_PATTERN.matcher(pattern);
113            StringBuffer text = new StringBuffer();
114            int requiredParameterCount = 0;
115            boolean err = false;
116            while (matcher.find()) {
117                int ndx = Integer.valueOf(matcher.group(1));
118                if (requiredParameterCount <= ndx) {
119                    requiredParameterCount = ndx + 1;
120                }
121                if (ndx >= parameters.length) {
122                    err = true;
123                    matcher.appendReplacement(text, matcher.group());
124                } else {
125                    Object parameter = parameters[ndx];
126                    matcher.appendReplacement(text, Matcher.quoteReplacement(parameter == null ? "null" : parameter.toString()));
127                }
128            }
129            if (err || requiredParameterCount < parameters.length) {
130                throw new IllegalArgumentException(
131                                                   CommonI18n.requiredToSuppliedParameterMismatch.text(parameters.length,
132                                                                                                       parameters.length == 1 ? "" : "s",
133                                                                                                       requiredParameterCount,
134                                                                                                       requiredParameterCount == 1 ? "" : "s",
135                                                                                                       pattern,
136                                                                                                       text.toString()));
137            }
138            matcher.appendTail(text);
139    
140            return text.toString();
141        }
142    
143        /**
144         * Create a new string containing the specified character repeated a specific number of times.
145         * 
146         * @param charToRepeat the character to repeat
147         * @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0
148         * @return the resulting string
149         */
150        public static String createString( final char charToRepeat,
151                                           int numberOfRepeats ) {
152            assert numberOfRepeats >= 0;
153            StringBuilder sb = new StringBuilder();
154            for (int i = 0; i < numberOfRepeats; ++i) {
155                sb.append(charToRepeat);
156            }
157            return sb.toString();
158        }
159    
160        /**
161         * Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or
162         * truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not
163         * remove leading and trailing whitespace.
164         * 
165         * @param original the string for which the length is to be set; may not be null
166         * @param length the desired length; must be positive
167         * @param padChar the character to use for padding, if the supplied string is not long enough
168         * @return the string of the desired length
169         * @see #justifyLeft(String, int, char)
170         */
171        public static String setLength( String original,
172                                        int length,
173                                        char padChar ) {
174            return justifyLeft(original, length, padChar, false);
175        }
176    
177        /**
178         * Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is
179         * longer than the desired width, the leading characters are removed so that the last character in the supplied string at the
180         * last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more
181         * times such that the last character in the supplied string appears as the last character in the resulting string and that
182         * the length matches that specified.
183         * 
184         * @param str the string to be right justified; if null, an empty string is used
185         * @param width the desired width of the string; must be positive
186         * @param padWithChar the character to use for padding, if needed
187         * @return the right justified string
188         */
189        public static String justifyRight( String str,
190                                           final int width,
191                                           char padWithChar ) {
192            assert width > 0;
193            // Trim the leading and trailing whitespace ...
194            str = str != null ? str.trim() : "";
195    
196            final int length = str.length();
197            int addChars = width - length;
198            if (addChars < 0) {
199                // truncate the first characters, keep the last
200                return str.subSequence(length - width, length).toString();
201            }
202            // Prepend the whitespace ...
203            final StringBuilder sb = new StringBuilder();
204            while (addChars > 0) {
205                sb.append(padWithChar);
206                --addChars;
207            }
208    
209            // Write the content ...
210            sb.append(str);
211            return sb.toString();
212        }
213    
214        /**
215         * Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the
216         * resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the
217         * specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of
218         * the string one or more times such that the length is that specified. All leading and trailing whitespace is removed.
219         * 
220         * @param str the string to be left justified; if null, an empty string is used
221         * @param width the desired width of the string; must be positive
222         * @param padWithChar the character to use for padding, if needed
223         * @return the left justified string
224         * @see #setLength(String, int, char)
225         */
226        public static String justifyLeft( String str,
227                                          final int width,
228                                          char padWithChar ) {
229            return justifyLeft(str, width, padWithChar, true);
230        }
231    
232        protected static String justifyLeft( String str,
233                                             final int width,
234                                             char padWithChar,
235                                             boolean trimWhitespace ) {
236            // Trim the leading and trailing whitespace ...
237            str = str != null ? (trimWhitespace ? str.trim() : str) : "";
238    
239            int addChars = width - str.length();
240            if (addChars < 0) {
241                // truncate
242                return str.subSequence(0, width).toString();
243            }
244            // Write the content ...
245            final StringBuilder sb = new StringBuilder();
246            sb.append(str);
247    
248            // Append the whitespace ...
249            while (addChars > 0) {
250                sb.append(padWithChar);
251                --addChars;
252            }
253    
254            return sb.toString();
255        }
256    
257        /**
258         * Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the
259         * specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning
260         * and end of the string such that the length is that specified; one additional padding character is prepended if required.
261         * All leading and trailing whitespace is removed before centering.
262         * 
263         * @param str the string to be left justified; if null, an empty string is used
264         * @param width the desired width of the string; must be positive
265         * @param padWithChar the character to use for padding, if needed
266         * @return the left justified string
267         * @see #setLength(String, int, char)
268         */
269        public static String justifyCenter( String str,
270                                            final int width,
271                                            char padWithChar ) {
272            // Trim the leading and trailing whitespace ...
273            str = str != null ? str.trim() : "";
274    
275            int addChars = width - str.length();
276            if (addChars < 0) {
277                // truncate
278                return str.subSequence(0, width).toString();
279            }
280            // Write the content ...
281            int prependNumber = addChars / 2;
282            int appendNumber = prependNumber;
283            if ((prependNumber + appendNumber) != addChars) {
284                ++prependNumber;
285            }
286    
287            final StringBuilder sb = new StringBuilder();
288    
289            // Prepend the pad character(s) ...
290            while (prependNumber > 0) {
291                sb.append(padWithChar);
292                --prependNumber;
293            }
294    
295            // Add the actual content
296            sb.append(str);
297    
298            // Append the pad character(s) ...
299            while (appendNumber > 0) {
300                sb.append(padWithChar);
301                --appendNumber;
302            }
303    
304            return sb.toString();
305        }
306    
307        /**
308         * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
309         * object is null.
310         * 
311         * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
312         * @param maxLength the maximum length of the string being returned
313         * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
314         *         maximum length (including the suffix)
315         * @throws IllegalArgumentException if the maximum length is negative
316         */
317        public static String truncate( Object obj,
318                                       int maxLength ) {
319            return truncate(obj, maxLength, null);
320        }
321    
322        /**
323         * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
324         * object is null.
325         * 
326         * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
327         * @param maxLength the maximum length of the string being returned
328         * @param suffix the suffix that should be added to the content if the string must be truncated, or null if the default suffix
329         *        of "..." should be used
330         * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
331         *         maximum length (including the suffix)
332         * @throws IllegalArgumentException if the maximum length is negative
333         */
334        public static String truncate( Object obj,
335                                       int maxLength,
336                                       String suffix ) {
337            CheckArg.isNonNegative(maxLength, "maxLength");
338            if (obj == null || maxLength == 0) {
339                return "";
340            }
341            String str = obj.toString();
342            if (str.length() <= maxLength) return str;
343            if (suffix == null) suffix = "...";
344            int maxNumChars = maxLength - suffix.length();
345            if (maxNumChars < 0) {
346                // Then the max length is actually shorter than the suffix ...
347                str = suffix.substring(0, maxLength);
348            } else if (str.length() > maxNumChars) {
349                str = str.substring(0, maxNumChars) + suffix;
350            }
351            return str;
352        }
353    
354        /**
355         * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished
356         * reading.
357         * 
358         * @param reader the reader of the contents; may be null
359         * @return the contents, or an empty string if the supplied reader is null
360         * @throws IOException if there is an error reading the content
361         */
362        public static String read( Reader reader ) throws IOException {
363            return IoUtil.read(reader);
364        }
365    
366        /**
367         * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
368         * reading.
369         * 
370         * @param stream the streamed contents; may be null
371         * @return the contents, or an empty string if the supplied stream is null
372         * @throws IOException if there is an error reading the content
373         */
374        public static String read( InputStream stream ) throws IOException {
375            return IoUtil.read(stream);
376        }
377    
378        /**
379         * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
380         * finished.
381         * 
382         * @param content the content to write to the stream; may be null
383         * @param stream the stream to which the content is to be written
384         * @throws IOException
385         * @throws IllegalArgumentException if the stream is null
386         */
387        public static void write( String content,
388                                  OutputStream stream ) throws IOException {
389            IoUtil.write(content, stream);
390        }
391    
392        /**
393         * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when
394         * finished.
395         * 
396         * @param content the content to write to the writer; may be null
397         * @param writer the writer to which the content is to be written
398         * @throws IOException
399         * @throws IllegalArgumentException if the writer is null
400         */
401        public static void write( String content,
402                                  Writer writer ) throws IOException {
403            IoUtil.write(content, writer);
404        }
405    
406        /**
407         * Get the stack trace of the supplied exception.
408         * 
409         * @param throwable the exception for which the stack trace is to be returned
410         * @return the stack trace, or null if the supplied exception is null
411         */
412        public static String getStackTrace( Throwable throwable ) {
413            if (throwable == null) return null;
414            final ByteArrayOutputStream bas = new ByteArrayOutputStream();
415            final PrintWriter pw = new PrintWriter(bas);
416            throwable.printStackTrace(pw);
417            pw.close();
418            return bas.toString();
419        }
420    
421        /**
422         * Removes leading and trailing whitespace from the supplied text, and reduces other consecutive whitespace characters to a
423         * single space. Whitespace includes line-feeds.
424         * 
425         * @param text the text to be normalized
426         * @return the normalized text
427         */
428        public static String normalize( String text ) {
429            CheckArg.isNotNull(text, "text");
430            // This could be much more efficient.
431            return NORMALIZE_PATTERN.matcher(text).replaceAll(" ").trim();
432        }
433    
434        private static final byte[] HEX_CHAR_TABLE = {(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6',
435            (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'};
436    
437        /**
438         * Get the hexadecimal string representation of the supplied byte array.
439         * 
440         * @param bytes the byte array
441         * @return the hex string representation of the byte array; never null
442         * @throws UnsupportedEncodingException
443         */
444        public static String getHexString( byte[] bytes ) throws UnsupportedEncodingException {
445            byte[] hex = new byte[2 * bytes.length];
446            int index = 0;
447    
448            for (byte b : bytes) {
449                int v = b & 0xFF;
450                hex[index++] = HEX_CHAR_TABLE[v >>> 4];
451                hex[index++] = HEX_CHAR_TABLE[v & 0xF];
452            }
453            return new String(hex, "ASCII");
454        }
455    
456        private StringUtil() {
457            // Prevent construction
458        }
459    }