View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6   * See the AUTHORS.txt file in the distribution for a full listing of 
7   * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.common.util;
25  
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.PrintWriter;
31  import java.io.Reader;
32  import java.io.UnsupportedEncodingException;
33  import java.io.Writer;
34  import java.math.BigInteger;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  import net.jcip.annotations.Immutable;
41  import org.modeshape.common.CommonI18n;
42  
43  /**
44   * Utilities for string processing and manipulation.
45   */
46  @Immutable
47  public class StringUtil {
48  
49      public static final String[] EMPTY_STRING_ARRAY = new String[0];
50      private static final Pattern NORMALIZE_PATTERN = Pattern.compile("\\s+");
51      private static final Pattern PARAMETER_COUNT_PATTERN = Pattern.compile("\\{(\\d+)\\}");
52  
53      /**
54       * Combine the lines into a single string, using the new line character as the delimiter. This is compatible with
55       * {@link #splitLines(String)}.
56       * 
57       * @param lines the lines to be combined
58       * @return the combined lines, or an empty string if there are no lines
59       */
60      public static String combineLines( String[] lines ) {
61          return combineLines(lines, '\n');
62      }
63  
64      /**
65       * Combine the lines into a single string, using the supplied separator as the delimiter.
66       * 
67       * @param lines the lines to be combined
68       * @param separator the separator character
69       * @return the combined lines, or an empty string if there are no lines
70       */
71      public static String combineLines( String[] lines,
72                                         char separator ) {
73          if (lines == null || lines.length == 0) return "";
74          StringBuilder sb = new StringBuilder();
75          for (int i = 0; i != lines.length; ++i) {
76              String line = lines[i];
77              if (i != 0) sb.append(separator);
78              sb.append(line);
79          }
80          return sb.toString();
81      }
82  
83      /**
84       * Split the supplied content into lines, returning each line as an element in the returned list.
85       * 
86       * @param content the string content that is to be split
87       * @return the list of lines; never null but may be an empty (unmodifiable) list if the supplied content is null or empty
88       */
89      public static List<String> splitLines( final String content ) {
90          if (content == null || content.length() == 0) return Collections.emptyList();
91          String[] lines = content.split("[\\r]?\\n");
92          return Arrays.asList(lines);
93      }
94  
95      /**
96       * Create a string by substituting the parameters into all key occurrences in the supplied format. The pattern consists of
97       * zero or more keys of the form <code>{n}</code>, where <code>n</code> is an integer starting at 1. Therefore, the first
98       * parameter replaces all occurrences of "{1}", the second parameter replaces all occurrences of "{2}", etc.
99       * <p>
100      * If any parameter is null, the corresponding key is replaced with the string "null". Therefore, consider using an empty
101      * string when keys are to be removed altogether.
102      * </p>
103      * <p>
104      * If there are no parameters, this method does nothing and returns the supplied pattern as is.
105      * </p>
106      * 
107      * @param pattern the pattern
108      * @param parameters the parameters used to replace keys
109      * @return the string with all keys replaced (or removed)
110      */
111     public static String createString( String pattern,
112                                        Object... parameters ) {
113         CheckArg.isNotNull(pattern, "pattern");
114         if (parameters == null) parameters = EMPTY_STRING_ARRAY;
115         Matcher matcher = PARAMETER_COUNT_PATTERN.matcher(pattern);
116         StringBuffer text = new StringBuffer();
117         int requiredParameterCount = 0;
118         boolean err = false;
119         while (matcher.find()) {
120             int ndx = Integer.valueOf(matcher.group(1));
121             if (requiredParameterCount <= ndx) {
122                 requiredParameterCount = ndx + 1;
123             }
124             if (ndx >= parameters.length) {
125                 err = true;
126                 matcher.appendReplacement(text, matcher.group());
127             } else {
128                 Object parameter = parameters[ndx];
129 
130                 // Automatically pretty-print arrays
131                 if (parameter != null && parameter.getClass().isArray()) {
132                     parameter = Arrays.asList((Object[])parameter);
133                 }
134 
135                 matcher.appendReplacement(text, Matcher.quoteReplacement(parameter == null ? "null" : parameter.toString()));
136             }
137         }
138         if (err || requiredParameterCount < parameters.length) {
139             throw new IllegalArgumentException(
140                                                CommonI18n.requiredToSuppliedParameterMismatch.text(parameters.length,
141                                                                                                    parameters.length == 1 ? "" : "s",
142                                                                                                    requiredParameterCount,
143                                                                                                    requiredParameterCount == 1 ? "" : "s",
144                                                                                                    pattern,
145                                                                                                    text.toString()));
146         }
147         matcher.appendTail(text);
148 
149         return text.toString();
150     }
151 
152     /**
153      * Create a new string containing the specified character repeated a specific number of times.
154      * 
155      * @param charToRepeat the character to repeat
156      * @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0
157      * @return the resulting string
158      */
159     public static String createString( final char charToRepeat,
160                                        int numberOfRepeats ) {
161         assert numberOfRepeats >= 0;
162         StringBuilder sb = new StringBuilder();
163         for (int i = 0; i < numberOfRepeats; ++i) {
164             sb.append(charToRepeat);
165         }
166         return sb.toString();
167     }
168 
169     /**
170      * Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or
171      * truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not
172      * remove leading and trailing whitespace.
173      * 
174      * @param original the string for which the length is to be set; may not be null
175      * @param length the desired length; must be positive
176      * @param padChar the character to use for padding, if the supplied string is not long enough
177      * @return the string of the desired length
178      * @see #justifyLeft(String, int, char)
179      */
180     public static String setLength( String original,
181                                     int length,
182                                     char padChar ) {
183         return justifyLeft(original, length, padChar, false);
184     }
185 
186     /**
187      * Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is
188      * longer than the desired width, the leading characters are removed so that the last character in the supplied string at the
189      * last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more
190      * times such that the last character in the supplied string appears as the last character in the resulting string and that
191      * the length matches that specified.
192      * 
193      * @param str the string to be right justified; if null, an empty string is used
194      * @param width the desired width of the string; must be positive
195      * @param padWithChar the character to use for padding, if needed
196      * @return the right justified string
197      */
198     public static String justifyRight( String str,
199                                        final int width,
200                                        char padWithChar ) {
201         assert width > 0;
202         // Trim the leading and trailing whitespace ...
203         str = str != null ? str.trim() : "";
204 
205         final int length = str.length();
206         int addChars = width - length;
207         if (addChars < 0) {
208             // truncate the first characters, keep the last
209             return str.subSequence(length - width, length).toString();
210         }
211         // Prepend the whitespace ...
212         final StringBuilder sb = new StringBuilder();
213         while (addChars > 0) {
214             sb.append(padWithChar);
215             --addChars;
216         }
217 
218         // Write the content ...
219         sb.append(str);
220         return sb.toString();
221     }
222 
223     /**
224      * Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the
225      * resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the
226      * specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of
227      * the string one or more times such that the length is that specified. All leading and trailing whitespace is removed.
228      * 
229      * @param str the string to be left justified; if null, an empty string is used
230      * @param width the desired width of the string; must be positive
231      * @param padWithChar the character to use for padding, if needed
232      * @return the left justified string
233      * @see #setLength(String, int, char)
234      */
235     public static String justifyLeft( String str,
236                                       final int width,
237                                       char padWithChar ) {
238         return justifyLeft(str, width, padWithChar, true);
239     }
240 
241     protected static String justifyLeft( String str,
242                                          final int width,
243                                          char padWithChar,
244                                          boolean trimWhitespace ) {
245         // Trim the leading and trailing whitespace ...
246         str = str != null ? (trimWhitespace ? str.trim() : str) : "";
247 
248         int addChars = width - str.length();
249         if (addChars < 0) {
250             // truncate
251             return str.subSequence(0, width).toString();
252         }
253         // Write the content ...
254         final StringBuilder sb = new StringBuilder();
255         sb.append(str);
256 
257         // Append the whitespace ...
258         while (addChars > 0) {
259             sb.append(padWithChar);
260             --addChars;
261         }
262 
263         return sb.toString();
264     }
265 
266     /**
267      * Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the
268      * specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning
269      * and end of the string such that the length is that specified; one additional padding character is prepended if required.
270      * All leading and trailing whitespace is removed before centering.
271      * 
272      * @param str the string to be left justified; if null, an empty string is used
273      * @param width the desired width of the string; must be positive
274      * @param padWithChar the character to use for padding, if needed
275      * @return the left justified string
276      * @see #setLength(String, int, char)
277      */
278     public static String justifyCenter( String str,
279                                         final int width,
280                                         char padWithChar ) {
281         // Trim the leading and trailing whitespace ...
282         str = str != null ? str.trim() : "";
283 
284         int addChars = width - str.length();
285         if (addChars < 0) {
286             // truncate
287             return str.subSequence(0, width).toString();
288         }
289         // Write the content ...
290         int prependNumber = addChars / 2;
291         int appendNumber = prependNumber;
292         if ((prependNumber + appendNumber) != addChars) {
293             ++prependNumber;
294         }
295 
296         final StringBuilder sb = new StringBuilder();
297 
298         // Prepend the pad character(s) ...
299         while (prependNumber > 0) {
300             sb.append(padWithChar);
301             --prependNumber;
302         }
303 
304         // Add the actual content
305         sb.append(str);
306 
307         // Append the pad character(s) ...
308         while (appendNumber > 0) {
309             sb.append(padWithChar);
310             --appendNumber;
311         }
312 
313         return sb.toString();
314     }
315 
316     /**
317      * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
318      * object is null.
319      * 
320      * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
321      * @param maxLength the maximum length of the string being returned
322      * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
323      *         maximum length (including the suffix)
324      * @throws IllegalArgumentException if the maximum length is negative
325      */
326     public static String truncate( Object obj,
327                                    int maxLength ) {
328         return truncate(obj, maxLength, null);
329     }
330 
331     /**
332      * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
333      * object is null.
334      * 
335      * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
336      * @param maxLength the maximum length of the string being returned
337      * @param suffix the suffix that should be added to the content if the string must be truncated, or null if the default suffix
338      *        of "..." should be used
339      * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
340      *         maximum length (including the suffix)
341      * @throws IllegalArgumentException if the maximum length is negative
342      */
343     public static String truncate( Object obj,
344                                    int maxLength,
345                                    String suffix ) {
346         CheckArg.isNonNegative(maxLength, "maxLength");
347         if (obj == null || maxLength == 0) {
348             return "";
349         }
350         String str = obj.toString();
351         if (str.length() <= maxLength) return str;
352         if (suffix == null) suffix = "...";
353         int maxNumChars = maxLength - suffix.length();
354         if (maxNumChars < 0) {
355             // Then the max length is actually shorter than the suffix ...
356             str = suffix.substring(0, maxLength);
357         } else if (str.length() > maxNumChars) {
358             str = str.substring(0, maxNumChars) + suffix;
359         }
360         return str;
361     }
362 
363     /**
364      * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished
365      * reading.
366      * 
367      * @param reader the reader of the contents; may be null
368      * @return the contents, or an empty string if the supplied reader is null
369      * @throws IOException if there is an error reading the content
370      */
371     public static String read( Reader reader ) throws IOException {
372         return IoUtil.read(reader);
373     }
374 
375     /**
376      * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
377      * reading.
378      * 
379      * @param stream the streamed contents; may be null
380      * @return the contents, or an empty string if the supplied stream is null
381      * @throws IOException if there is an error reading the content
382      */
383     public static String read( InputStream stream ) throws IOException {
384         return IoUtil.read(stream);
385     }
386 
387     /**
388      * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
389      * finished.
390      * 
391      * @param content the content to write to the stream; may be null
392      * @param stream the stream to which the content is to be written
393      * @throws IOException
394      * @throws IllegalArgumentException if the stream is null
395      */
396     public static void write( String content,
397                               OutputStream stream ) throws IOException {
398         IoUtil.write(content, stream);
399     }
400 
401     /**
402      * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when
403      * finished.
404      * 
405      * @param content the content to write to the writer; may be null
406      * @param writer the writer to which the content is to be written
407      * @throws IOException
408      * @throws IllegalArgumentException if the writer is null
409      */
410     public static void write( String content,
411                               Writer writer ) throws IOException {
412         IoUtil.write(content, writer);
413     }
414 
415     /**
416      * Get the stack trace of the supplied exception.
417      * 
418      * @param throwable the exception for which the stack trace is to be returned
419      * @return the stack trace, or null if the supplied exception is null
420      */
421     public static String getStackTrace( Throwable throwable ) {
422         if (throwable == null) return null;
423         final ByteArrayOutputStream bas = new ByteArrayOutputStream();
424         final PrintWriter pw = new PrintWriter(bas);
425         throwable.printStackTrace(pw);
426         pw.close();
427         return bas.toString();
428     }
429 
430     /**
431      * Removes leading and trailing whitespace from the supplied text, and reduces other consecutive whitespace characters to a
432      * single space. Whitespace includes line-feeds.
433      * 
434      * @param text the text to be normalized
435      * @return the normalized text
436      */
437     public static String normalize( String text ) {
438         CheckArg.isNotNull(text, "text");
439         // This could be much more efficient.
440         return NORMALIZE_PATTERN.matcher(text).replaceAll(" ").trim();
441     }
442 
443     private static final byte[] HEX_CHAR_TABLE = {(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6',
444         (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'};
445 
446     /**
447      * Get the hexadecimal string representation of the supplied byte array.
448      * 
449      * @param bytes the byte array
450      * @return the hex string representation of the byte array; never null
451      */
452     public static String getHexString( byte[] bytes ) {
453         try {
454             byte[] hex = new byte[2 * bytes.length];
455             int index = 0;
456 
457             for (byte b : bytes) {
458                 int v = b & 0xFF;
459                 hex[index++] = HEX_CHAR_TABLE[v >>> 4];
460                 hex[index++] = HEX_CHAR_TABLE[v & 0xF];
461             }
462             return new String(hex, "ASCII");
463         } catch (UnsupportedEncodingException e) {
464             BigInteger bi = new BigInteger(1, bytes);
465             return String.format("%0" + (bytes.length << 1) + "x", bi);
466         }
467     }
468 
469     public static byte[] fromHexString( String hexadecimal ) {
470         int len = hexadecimal.length();
471         if (len % 2 != 0) {
472             hexadecimal = "0" + hexadecimal;
473         }
474         byte[] data = new byte[len / 2];
475         for (int i = 0; i < len; i += 2) {
476             data[i / 2] = (byte)((Character.digit(hexadecimal.charAt(i), 16) << 4) + Character.digit(hexadecimal.charAt(i + 1),
477                                                                                                      16));
478         }
479         return data;
480     }
481 
482     private StringUtil() {
483         // Prevent construction
484     }
485 }