001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.util;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.io.PrintWriter;
023import java.io.Reader;
024import java.io.UnsupportedEncodingException;
025import java.io.Writer;
026import java.lang.reflect.Array;
027import java.math.BigInteger;
028import java.text.CharacterIterator;
029import java.text.StringCharacterIterator;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.List;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036import org.modeshape.common.CommonI18n;
037import org.modeshape.common.annotation.Immutable;
038
039/**
040 * Utilities for string processing and manipulation.
041 */
042@Immutable
043public class StringUtil {
044
045    public static final String[] EMPTY_STRING_ARRAY = new String[0];
046    private static final Pattern NORMALIZE_PATTERN = Pattern.compile("\\s+");
047    private static final Pattern PARAMETER_COUNT_PATTERN = Pattern.compile("\\{(\\d+)\\}");
048
049    /**
050     * Combine the lines into a single string, using the new line character as the delimiter. This is compatible with
051     * {@link #splitLines(String)}.
052     * 
053     * @param lines the lines to be combined
054     * @return the combined lines, or an empty string if there are no lines
055     */
056    public static String combineLines( String[] lines ) {
057        return combineLines(lines, '\n');
058    }
059
060    /**
061     * Combine the lines into a single string, using the supplied separator as the delimiter.
062     * 
063     * @param lines the lines to be combined
064     * @param separator the separator character
065     * @return the combined lines, or an empty string if there are no lines
066     */
067    public static String combineLines( String[] lines,
068                                       char separator ) {
069        if (lines == null || lines.length == 0) return "";
070        StringBuilder sb = new StringBuilder();
071        for (int i = 0; i != lines.length; ++i) {
072            String line = lines[i];
073            if (i != 0) sb.append(separator);
074            sb.append(line);
075        }
076        return sb.toString();
077    }
078
079    /**
080     * Split the supplied content into lines, returning each line as an element in the returned list.
081     * 
082     * @param content the string content that is to be split
083     * @return the list of lines; never null but may be an empty (unmodifiable) list if the supplied content is null or empty
084     */
085    public static List<String> splitLines( final String content ) {
086        if (content == null || content.length() == 0) return Collections.emptyList();
087        String[] lines = content.split("[\\r]?\\n");
088        return Arrays.asList(lines);
089    }
090
091    /**
092     * Combine the supplied values into a single string, using the supplied string to delimit values.
093     * 
094     * @param values the values to be combined; may not be null, but may contain null values (which are skipped)
095     * @param delimiter the characters to place between each of the values; may not be null
096     * @return the joined string; never null
097     * @see String#split(String)
098     */
099    public static String join( Object[] values,
100                               String delimiter ) {
101        StringBuilder sb = new StringBuilder();
102        boolean first = true;
103        for (Object segment : values) {
104            if (segment == null) continue;
105            if (first) first = false;
106            else sb.append(delimiter);
107            sb.append(segment);
108        }
109        return sb.toString();
110    }
111
112    /**
113     * Combine the supplied values into a single string, using the supplied string to delimit values.
114     * 
115     * @param values the values to be combined; may not be null, but may contain null values (which are skipped)
116     * @param delimiter the characters to place between each of the values; may not be null
117     * @return the joined string; never null
118     * @see String#split(String)
119     */
120    public static String join( Iterable<?> values,
121                               String delimiter ) {
122        StringBuilder sb = new StringBuilder();
123        boolean first = true;
124        for (Object segment : values) {
125            if (segment == null) continue;
126            if (first) first = false;
127            else sb.append(delimiter);
128            sb.append(segment);
129        }
130        return sb.toString();
131    }
132
133    /**
134     * Create a string by substituting the parameters into all key occurrences in the supplied format. The pattern consists of
135     * zero or more keys of the form <code>{n}</code>, where <code>n</code> is an integer starting at 0. Therefore, the first
136     * parameter replaces all occurrences of "{0}", the second parameter replaces all occurrences of "{1}", etc.
137     * <p>
138     * If any parameter is null, the corresponding key is replaced with the string "null". Therefore, consider using an empty
139     * string when keys are to be removed altogether.
140     * </p>
141     * <p>
142     * If there are no parameters, this method does nothing and returns the supplied pattern as is.
143     * </p>
144     * 
145     * @param pattern the pattern
146     * @param parameters the parameters used to replace keys
147     * @return the string with all keys replaced (or removed)
148     */
149    public static String createString( String pattern,
150                                       Object... parameters ) {
151        CheckArg.isNotNull(pattern, "pattern");
152        if (parameters == null) parameters = EMPTY_STRING_ARRAY;
153        Matcher matcher = PARAMETER_COUNT_PATTERN.matcher(pattern);
154        // CHECKSTYLE IGNORE check FOR NEXT 1 LINES
155        StringBuffer text = new StringBuffer();
156        int requiredParameterCount = 0;
157        boolean err = false;
158        while (matcher.find()) {
159            int ndx = Integer.valueOf(matcher.group(1));
160            if (requiredParameterCount <= ndx) {
161                requiredParameterCount = ndx + 1;
162            }
163            if (ndx >= parameters.length) {
164                err = true;
165                matcher.appendReplacement(text, matcher.group());
166            } else {
167                Object parameter = parameters[ndx];
168
169                // Automatically pretty-print arrays
170                if (parameter != null && parameter.getClass().isArray()) {
171                    if (parameter instanceof Object[]) {
172                        parameter = Arrays.asList((Object[])parameter);
173                    } else {
174                        int length = Array.getLength(parameter);
175                        List<Object> parameterAsList = new ArrayList<Object>(length);
176                        for (int i = 0; i < length; i++) {
177                            parameterAsList.add(Array.get(parameter, i));
178                        }
179                        parameter = parameterAsList;
180                    }
181                }
182
183                matcher.appendReplacement(text, Matcher.quoteReplacement(parameter == null ? "null" : parameter.toString()));
184            }
185        }
186        if (err || requiredParameterCount < parameters.length) {
187            throw new IllegalArgumentException(
188                                               CommonI18n.requiredToSuppliedParameterMismatch.text(parameters.length,
189                                                                                                   parameters.length == 1 ? "" : "s",
190                                                                                                   requiredParameterCount,
191                                                                                                   requiredParameterCount == 1 ? "" : "s",
192                                                                                                   pattern, text.toString()));
193        }
194        matcher.appendTail(text);
195
196        return text.toString();
197    }
198
199    /**
200     * Create a new string containing the specified character repeated a specific number of times.
201     * 
202     * @param charToRepeat the character to repeat
203     * @param numberOfRepeats the number of times the character is to repeat in the result; must be greater than 0
204     * @return the resulting string
205     */
206    public static String createString( final char charToRepeat,
207                                       int numberOfRepeats ) {
208        assert numberOfRepeats >= 0;
209        StringBuilder sb = new StringBuilder();
210        for (int i = 0; i < numberOfRepeats; ++i) {
211            sb.append(charToRepeat);
212        }
213        return sb.toString();
214    }
215
216    /**
217     * Set the length of the string, padding with the supplied character if the supplied string is shorter than desired, or
218     * truncating the string if it is longer than desired. Unlike {@link #justifyLeft(String, int, char)}, this method does not
219     * remove leading and trailing whitespace.
220     * 
221     * @param original the string for which the length is to be set; may not be null
222     * @param length the desired length; must be positive
223     * @param padChar the character to use for padding, if the supplied string is not long enough
224     * @return the string of the desired length
225     * @see #justifyLeft(String, int, char)
226     */
227    public static String setLength( String original,
228                                    int length,
229                                    char padChar ) {
230        return justifyLeft(original, length, padChar, false);
231    }
232
233    public static enum Justify {
234        LEFT,
235        RIGHT,
236        CENTER;
237    }
238
239    /**
240     * Justify the contents of the string.
241     * 
242     * @param justify the way in which the string is to be justified
243     * @param str the string to be right justified; if null, an empty string is used
244     * @param width the desired width of the string; must be positive
245     * @param padWithChar the character to use for padding, if needed
246     * @return the right justified string
247     */
248    public static String justify( Justify justify,
249                                  String str,
250                                  final int width,
251                                  char padWithChar ) {
252        switch (justify) {
253            case LEFT:
254                return justifyLeft(str, width, padWithChar);
255            case RIGHT:
256                return justifyRight(str, width, padWithChar);
257            case CENTER:
258                return justifyCenter(str, width, padWithChar);
259        }
260        assert false;
261        return null;
262    }
263
264    /**
265     * Right justify the contents of the string, ensuring that the string ends at the last character. If the supplied string is
266     * longer than the desired width, the leading characters are removed so that the last character in the supplied string at the
267     * last position. If the supplied string is shorter than the desired width, the padding character is inserted one or more
268     * times such that the last character in the supplied string appears as the last character in the resulting string and that
269     * the length matches that specified.
270     * 
271     * @param str the string to be right justified; if null, an empty string is used
272     * @param width the desired width of the string; must be positive
273     * @param padWithChar the character to use for padding, if needed
274     * @return the right justified string
275     */
276    public static String justifyRight( String str,
277                                       final int width,
278                                       char padWithChar ) {
279        assert width > 0;
280        // Trim the leading and trailing whitespace ...
281        str = str != null ? str.trim() : "";
282
283        final int length = str.length();
284        int addChars = width - length;
285        if (addChars < 0) {
286            // truncate the first characters, keep the last
287            return str.subSequence(length - width, length).toString();
288        }
289        // Prepend the whitespace ...
290        final StringBuilder sb = new StringBuilder();
291        while (addChars > 0) {
292            sb.append(padWithChar);
293            --addChars;
294        }
295
296        // Write the content ...
297        sb.append(str);
298        return sb.toString();
299    }
300
301    /**
302     * Left justify the contents of the string, ensuring that the supplied string begins at the first character and that the
303     * resulting string is of the desired length. If the supplied string is longer than the desired width, it is truncated to the
304     * specified length. If the supplied string is shorter than the desired width, the padding character is added to the end of
305     * the string one or more times such that the length is that specified. All leading and trailing whitespace is removed.
306     * 
307     * @param str the string to be left justified; if null, an empty string is used
308     * @param width the desired width of the string; must be positive
309     * @param padWithChar the character to use for padding, if needed
310     * @return the left justified string
311     * @see #setLength(String, int, char)
312     */
313    public static String justifyLeft( String str,
314                                      final int width,
315                                      char padWithChar ) {
316        return justifyLeft(str, width, padWithChar, true);
317    }
318
319    protected static String justifyLeft( String str,
320                                         final int width,
321                                         char padWithChar,
322                                         boolean trimWhitespace ) {
323        // Trim the leading and trailing whitespace ...
324        str = str != null ? (trimWhitespace ? str.trim() : str) : "";
325
326        int addChars = width - str.length();
327        if (addChars < 0) {
328            // truncate
329            return str.subSequence(0, width).toString();
330        }
331        // Write the content ...
332        final StringBuilder sb = new StringBuilder();
333        sb.append(str);
334
335        // Append the whitespace ...
336        while (addChars > 0) {
337            sb.append(padWithChar);
338            --addChars;
339        }
340
341        return sb.toString();
342    }
343
344    /**
345     * Center the contents of the string. If the supplied string is longer than the desired width, it is truncated to the
346     * specified length. If the supplied string is shorter than the desired width, padding characters are added to the beginning
347     * and end of the string such that the length is that specified; one additional padding character is prepended if required.
348     * All leading and trailing whitespace is removed before centering.
349     * 
350     * @param str the string to be left justified; if null, an empty string is used
351     * @param width the desired width of the string; must be positive
352     * @param padWithChar the character to use for padding, if needed
353     * @return the left justified string
354     * @see #setLength(String, int, char)
355     */
356    public static String justifyCenter( String str,
357                                        final int width,
358                                        char padWithChar ) {
359        // Trim the leading and trailing whitespace ...
360        str = str != null ? str.trim() : "";
361
362        int addChars = width - str.length();
363        if (addChars < 0) {
364            // truncate
365            return str.subSequence(0, width).toString();
366        }
367        // Write the content ...
368        int prependNumber = addChars / 2;
369        int appendNumber = prependNumber;
370        if ((prependNumber + appendNumber) != addChars) {
371            ++prependNumber;
372        }
373
374        final StringBuilder sb = new StringBuilder();
375
376        // Prepend the pad character(s) ...
377        while (prependNumber > 0) {
378            sb.append(padWithChar);
379            --prependNumber;
380        }
381
382        // Add the actual content
383        sb.append(str);
384
385        // Append the pad character(s) ...
386        while (appendNumber > 0) {
387            sb.append(padWithChar);
388            --appendNumber;
389        }
390
391        return sb.toString();
392    }
393
394    /**
395     * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
396     * object is null.
397     * 
398     * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
399     * @param maxLength the maximum length of the string being returned
400     * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
401     *         maximum length (including the suffix)
402     * @throws IllegalArgumentException if the maximum length is negative
403     */
404    public static String truncate( Object obj,
405                                   int maxLength ) {
406        return truncate(obj, maxLength, null);
407    }
408
409    /**
410     * Truncate the supplied string to be no more than the specified length. This method returns an empty string if the supplied
411     * object is null.
412     * 
413     * @param obj the object from which the string is to be obtained using {@link Object#toString()}.
414     * @param maxLength the maximum length of the string being returned
415     * @param suffix the suffix that should be added to the content if the string must be truncated, or null if the default suffix
416     *        of "..." should be used
417     * @return the supplied string if no longer than the maximum length, or the supplied string truncated to be no longer than the
418     *         maximum length (including the suffix)
419     * @throws IllegalArgumentException if the maximum length is negative
420     */
421    public static String truncate( Object obj,
422                                   int maxLength,
423                                   String suffix ) {
424        CheckArg.isNonNegative(maxLength, "maxLength");
425        if (obj == null || maxLength == 0) {
426            return "";
427        }
428        String str = obj.toString();
429        if (str.length() <= maxLength) return str;
430        if (suffix == null) suffix = "...";
431        int maxNumChars = maxLength - suffix.length();
432        if (maxNumChars < 0) {
433            // Then the max length is actually shorter than the suffix ...
434            str = suffix.substring(0, maxLength);
435        } else if (str.length() > maxNumChars) {
436            str = str.substring(0, maxNumChars) + suffix;
437        }
438        return str;
439    }
440
441    /**
442     * Read and return the entire contents of the supplied {@link Reader}. This method always closes the reader when finished
443     * reading.
444     * 
445     * @param reader the reader of the contents; may be null
446     * @return the contents, or an empty string if the supplied reader is null
447     * @throws IOException if there is an error reading the content
448     */
449    public static String read( Reader reader ) throws IOException {
450        return IoUtil.read(reader);
451    }
452
453    /**
454     * Read and return the entire contents of the supplied {@link InputStream}. This method always closes the stream when finished
455     * reading.
456     * 
457     * @param stream the streamed contents; may be null
458     * @return the contents, or an empty string if the supplied stream is null
459     * @throws IOException if there is an error reading the content
460     */
461    public static String read( InputStream stream ) throws IOException {
462        return IoUtil.read(stream);
463    }
464
465    /**
466     * Write the entire contents of the supplied string to the given stream. This method always flushes and closes the stream when
467     * finished.
468     * 
469     * @param content the content to write to the stream; may be null
470     * @param stream the stream to which the content is to be written
471     * @throws IOException
472     * @throws IllegalArgumentException if the stream is null
473     */
474    public static void write( String content,
475                              OutputStream stream ) throws IOException {
476        IoUtil.write(content, stream);
477    }
478
479    /**
480     * Write the entire contents of the supplied string to the given writer. This method always flushes and closes the writer when
481     * finished.
482     * 
483     * @param content the content to write to the writer; may be null
484     * @param writer the writer to which the content is to be written
485     * @throws IOException
486     * @throws IllegalArgumentException if the writer is null
487     */
488    public static void write( String content,
489                              Writer writer ) throws IOException {
490        IoUtil.write(content, writer);
491    }
492
493    /**
494     * Get the stack trace of the supplied exception.
495     * 
496     * @param throwable the exception for which the stack trace is to be returned
497     * @return the stack trace, or null if the supplied exception is null
498     */
499    public static String getStackTrace( Throwable throwable ) {
500        if (throwable == null) return null;
501        final ByteArrayOutputStream bas = new ByteArrayOutputStream();
502        final PrintWriter pw = new PrintWriter(bas);
503        throwable.printStackTrace(pw);
504        pw.close();
505        return bas.toString();
506    }
507
508    /**
509     * Removes leading and trailing whitespace from the supplied text, and reduces other consecutive whitespace characters to a
510     * single space. Whitespace includes line-feeds.
511     * 
512     * @param text the text to be normalized
513     * @return the normalized text
514     */
515    public static String normalize( String text ) {
516        CheckArg.isNotNull(text, "text");
517        // This could be much more efficient.
518        return NORMALIZE_PATTERN.matcher(text).replaceAll(" ").trim();
519    }
520
521    private static final byte[] HEX_CHAR_TABLE = {(byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6',
522        (byte)'7', (byte)'8', (byte)'9', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f'};
523
524    /**
525     * Get the hexadecimal string representation of the supplied byte array.
526     * 
527     * @param bytes the byte array
528     * @return the hex string representation of the byte array; never null
529     */
530    public static String getHexString( byte[] bytes ) {
531        try {
532            byte[] hex = new byte[2 * bytes.length];
533            int index = 0;
534
535            for (byte b : bytes) {
536                int v = b & 0xFF;
537                hex[index++] = HEX_CHAR_TABLE[v >>> 4];
538                hex[index++] = HEX_CHAR_TABLE[v & 0xF];
539            }
540            return new String(hex, "ASCII");
541        } catch (UnsupportedEncodingException e) {
542            BigInteger bi = new BigInteger(1, bytes);
543            return String.format("%0" + (bytes.length << 1) + "x", bi);
544        }
545    }
546
547    public static byte[] fromHexString( String hexadecimal ) {
548        int len = hexadecimal.length();
549        if (len % 2 != 0) {
550            hexadecimal = "0" + hexadecimal;
551        }
552        byte[] data = new byte[len / 2];
553        for (int i = 0; i < len; i += 2) {
554            data[i / 2] = (byte)((Character.digit(hexadecimal.charAt(i), 16) << 4) + Character.digit(hexadecimal.charAt(i + 1),
555                                                                                                     16));
556        }
557        return data;
558    }
559
560    public static boolean isHexString( String hexadecimal ) {
561        int len = hexadecimal.length();
562        for (int i = 0; i < len; ++i) {
563            if (!isHexCharacter(hexadecimal.charAt(i))) return false;
564        }
565        return true;
566    }
567
568    public static boolean isHexCharacter( char c ) {
569        return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
570    }
571
572    /**
573     * Returns true if the given string is null or represents the empty string
574     * 
575     * @param str the string; may be null or empty
576     * @return true if the string is null or contains only whitespace
577     */
578    public static boolean isBlank( String str ) {
579        return str == null || str.trim().isEmpty();
580    } 
581    
582    /**
583     * Returns true if the given string is not null and does not represents the empty string
584     * 
585     * @param str the string; may be null or empty
586     * @return true if the string is not null and not empty, false otherwise
587     */
588    public static boolean notBlank( String str ) {
589        return !isBlank(str);
590    }
591
592    /**
593     * Return whether the supplied string contains any of the supplied characters.
594     * 
595     * @param str the string to be examined; may not be null
596     * @param chars the characters to be found within the supplied string; may be zero-length
597     * @return true if the supplied string contains at least one of the supplied characters, or false otherwise
598     */
599    public static boolean containsAnyOf( String str,
600                                         char... chars ) {
601        CharacterIterator iter = new StringCharacterIterator(str);
602        for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
603            for (char match : chars) {
604                if (c == match) return true;
605            }
606        }
607        return false;
608    }
609
610    private StringUtil() {
611        // Prevent construction
612    }
613}