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