001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.util;
020
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.net.URI;
024import java.net.URISyntaxException;
025import java.text.DateFormat;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.Date;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Locale;
033import java.util.Map;
034import java.util.StringTokenizer;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038import org.apache.commons.lang.SystemUtils;
039import org.apache.hadoop.classification.InterfaceAudience;
040import org.apache.hadoop.classification.InterfaceStability;
041import org.apache.hadoop.fs.Path;
042import org.apache.hadoop.net.NetUtils;
043
044import com.google.common.net.InetAddresses;
045
046/**
047 * General string utils
048 */
049@InterfaceAudience.Private
050@InterfaceStability.Unstable
051public class StringUtils {
052
053  /**
054   * Priority of the StringUtils shutdown hook.
055   */
056  public static final int SHUTDOWN_HOOK_PRIORITY = 0;
057
058  /**
059   * Shell environment variables: $ followed by one letter or _ followed by
060   * multiple letters, numbers, or underscores.  The group captures the
061   * environment variable name without the leading $.
062   */
063  public static final Pattern SHELL_ENV_VAR_PATTERN =
064    Pattern.compile("\\$([A-Za-z_]{1}[A-Za-z0-9_]*)");
065
066  /**
067   * Windows environment variables: surrounded by %.  The group captures the
068   * environment variable name without the leading and trailing %.
069   */
070  public static final Pattern WIN_ENV_VAR_PATTERN = Pattern.compile("%(.*?)%");
071
072  /**
073   * Regular expression that matches and captures environment variable names
074   * according to platform-specific rules.
075   */
076  public static final Pattern ENV_VAR_PATTERN = Shell.WINDOWS ?
077    WIN_ENV_VAR_PATTERN : SHELL_ENV_VAR_PATTERN;
078
079  /**
080   * Make a string representation of the exception.
081   * @param e The exception to stringify
082   * @return A string with exception name and call stack.
083   */
084  public static String stringifyException(Throwable e) {
085    StringWriter stm = new StringWriter();
086    PrintWriter wrt = new PrintWriter(stm);
087    e.printStackTrace(wrt);
088    wrt.close();
089    return stm.toString();
090  }
091  
092  /**
093   * Given a full hostname, return the word upto the first dot.
094   * @param fullHostname the full hostname
095   * @return the hostname to the first dot
096   */
097  public static String simpleHostname(String fullHostname) {
098    if (InetAddresses.isInetAddress(fullHostname)) {
099      return fullHostname;
100    }
101    int offset = fullHostname.indexOf('.');
102    if (offset != -1) {
103      return fullHostname.substring(0, offset);
104    }
105    return fullHostname;
106  }
107  
108  /**
109   * Given an integer, return a string that is in an approximate, but human 
110   * readable format. 
111   * @param number the number to format
112   * @return a human readable form of the integer
113   *
114   * @deprecated use {@link TraditionalBinaryPrefix#long2String(long, String, int)}.
115   */
116  @Deprecated
117  public static String humanReadableInt(long number) {
118    return TraditionalBinaryPrefix.long2String(number, "", 1);
119  }
120
121  /** The same as String.format(Locale.ENGLISH, format, objects). */
122  public static String format(final String format, final Object... objects) {
123    return String.format(Locale.ENGLISH, format, objects);
124  }
125
126  /**
127   * Format a percentage for presentation to the user.
128   * @param fraction the percentage as a fraction, e.g. 0.1 = 10%
129   * @param decimalPlaces the number of decimal places
130   * @return a string representation of the percentage
131   */
132  public static String formatPercent(double fraction, int decimalPlaces) {
133    return format("%." + decimalPlaces + "f%%", fraction*100);
134  }
135  
136  /**
137   * Given an array of strings, return a comma-separated list of its elements.
138   * @param strs Array of strings
139   * @return Empty string if strs.length is 0, comma separated list of strings
140   * otherwise
141   */
142  
143  public static String arrayToString(String[] strs) {
144    if (strs.length == 0) { return ""; }
145    StringBuilder sbuf = new StringBuilder();
146    sbuf.append(strs[0]);
147    for (int idx = 1; idx < strs.length; idx++) {
148      sbuf.append(",");
149      sbuf.append(strs[idx]);
150    }
151    return sbuf.toString();
152  }
153
154  /**
155   * Given an array of bytes it will convert the bytes to a hex string
156   * representation of the bytes
157   * @param bytes
158   * @param start start index, inclusively
159   * @param end end index, exclusively
160   * @return hex string representation of the byte array
161   */
162  public static String byteToHexString(byte[] bytes, int start, int end) {
163    if (bytes == null) {
164      throw new IllegalArgumentException("bytes == null");
165    }
166    StringBuilder s = new StringBuilder(); 
167    for(int i = start; i < end; i++) {
168      s.append(format("%02x", bytes[i]));
169    }
170    return s.toString();
171  }
172
173  /** Same as byteToHexString(bytes, 0, bytes.length). */
174  public static String byteToHexString(byte bytes[]) {
175    return byteToHexString(bytes, 0, bytes.length);
176  }
177
178  /**
179   * Given a hexstring this will return the byte array corresponding to the
180   * string
181   * @param hex the hex String array
182   * @return a byte array that is a hex string representation of the given
183   *         string. The size of the byte array is therefore hex.length/2
184   */
185  public static byte[] hexStringToByte(String hex) {
186    byte[] bts = new byte[hex.length() / 2];
187    for (int i = 0; i < bts.length; i++) {
188      bts[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
189    }
190    return bts;
191  }
192  /**
193   * 
194   * @param uris
195   */
196  public static String uriToString(URI[] uris){
197    if (uris == null) {
198      return null;
199    }
200    StringBuilder ret = new StringBuilder(uris[0].toString());
201    for(int i = 1; i < uris.length;i++){
202      ret.append(",");
203      ret.append(uris[i].toString());
204    }
205    return ret.toString();
206  }
207  
208  /**
209   * @param str
210   *          The string array to be parsed into an URI array.
211   * @return <tt>null</tt> if str is <tt>null</tt>, else the URI array
212   *         equivalent to str.
213   * @throws IllegalArgumentException
214   *           If any string in str violates RFC&nbsp;2396.
215   */
216  public static URI[] stringToURI(String[] str){
217    if (str == null) 
218      return null;
219    URI[] uris = new URI[str.length];
220    for (int i = 0; i < str.length;i++){
221      try{
222        uris[i] = new URI(str[i]);
223      }catch(URISyntaxException ur){
224        throw new IllegalArgumentException(
225            "Failed to create uri for " + str[i], ur);
226      }
227    }
228    return uris;
229  }
230  
231  /**
232   * 
233   * @param str
234   */
235  public static Path[] stringToPath(String[] str){
236    if (str == null) {
237      return null;
238    }
239    Path[] p = new Path[str.length];
240    for (int i = 0; i < str.length;i++){
241      p[i] = new Path(str[i]);
242    }
243    return p;
244  }
245  /**
246   * 
247   * Given a finish and start time in long milliseconds, returns a 
248   * String in the format Xhrs, Ymins, Z sec, for the time difference between two times. 
249   * If finish time comes before start time then negative valeus of X, Y and Z wil return. 
250   * 
251   * @param finishTime finish time
252   * @param startTime start time
253   */
254  public static String formatTimeDiff(long finishTime, long startTime){
255    long timeDiff = finishTime - startTime; 
256    return formatTime(timeDiff); 
257  }
258  
259  /**
260   * 
261   * Given the time in long milliseconds, returns a 
262   * String in the format Xhrs, Ymins, Z sec. 
263   * 
264   * @param timeDiff The time difference to format
265   */
266  public static String formatTime(long timeDiff){
267    StringBuilder buf = new StringBuilder();
268    long hours = timeDiff / (60*60*1000);
269    long rem = (timeDiff % (60*60*1000));
270    long minutes =  rem / (60*1000);
271    rem = rem % (60*1000);
272    long seconds = rem / 1000;
273    
274    if (hours != 0){
275      buf.append(hours);
276      buf.append("hrs, ");
277    }
278    if (minutes != 0){
279      buf.append(minutes);
280      buf.append("mins, ");
281    }
282    // return "0sec if no difference
283    buf.append(seconds);
284    buf.append("sec");
285    return buf.toString(); 
286  }
287  /**
288   * Formats time in ms and appends difference (finishTime - startTime) 
289   * as returned by formatTimeDiff().
290   * If finish time is 0, empty string is returned, if start time is 0 
291   * then difference is not appended to return value. 
292   * @param dateFormat date format to use
293   * @param finishTime fnish time
294   * @param startTime start time
295   * @return formatted value. 
296   */
297  public static String getFormattedTimeWithDiff(DateFormat dateFormat, 
298                                                long finishTime, long startTime){
299    StringBuilder buf = new StringBuilder();
300    if (0 != finishTime) {
301      buf.append(dateFormat.format(new Date(finishTime)));
302      if (0 != startTime){
303        buf.append(" (" + formatTimeDiff(finishTime , startTime) + ")");
304      }
305    }
306    return buf.toString();
307  }
308  
309  /**
310   * Returns an arraylist of strings.
311   * @param str the comma seperated string values
312   * @return the arraylist of the comma seperated string values
313   */
314  public static String[] getStrings(String str){
315    Collection<String> values = getStringCollection(str);
316    if(values.size() == 0) {
317      return null;
318    }
319    return values.toArray(new String[values.size()]);
320  }
321
322  /**
323   * Returns a collection of strings.
324   * @param str comma seperated string values
325   * @return an <code>ArrayList</code> of string values
326   */
327  public static Collection<String> getStringCollection(String str){
328    String delim = ",";
329    return getStringCollection(str, delim);
330  }
331
332  /**
333   * Returns a collection of strings.
334   * 
335   * @param str
336   *          String to parse
337   * @param delim
338   *          delimiter to separate the values
339   * @return Collection of parsed elements.
340   */
341  public static Collection<String> getStringCollection(String str, String delim) {
342    List<String> values = new ArrayList<String>();
343    if (str == null)
344      return values;
345    StringTokenizer tokenizer = new StringTokenizer(str, delim);
346    while (tokenizer.hasMoreTokens()) {
347      values.add(tokenizer.nextToken());
348    }
349    return values;
350  }
351
352  /**
353   * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
354   * @param str a comma separated <String> with values
355   * @return a <code>Collection</code> of <code>String</code> values
356   */
357  public static Collection<String> getTrimmedStringCollection(String str){
358    return new ArrayList<String>(
359      Arrays.asList(getTrimmedStrings(str)));
360  }
361  
362  /**
363   * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
364   * @param str a comma separated <String> with values
365   * @return an array of <code>String</code> values
366   */
367  public static String[] getTrimmedStrings(String str){
368    if (null == str || str.trim().isEmpty()) {
369      return emptyStringArray;
370    }
371
372    return str.trim().split("\\s*,\\s*");
373  }
374
375  final public static String[] emptyStringArray = {};
376  final public static char COMMA = ',';
377  final public static String COMMA_STR = ",";
378  final public static char ESCAPE_CHAR = '\\';
379  
380  /**
381   * Split a string using the default separator
382   * @param str a string that may have escaped separator
383   * @return an array of strings
384   */
385  public static String[] split(String str) {
386    return split(str, ESCAPE_CHAR, COMMA);
387  }
388  
389  /**
390   * Split a string using the given separator
391   * @param str a string that may have escaped separator
392   * @param escapeChar a char that be used to escape the separator
393   * @param separator a separator char
394   * @return an array of strings
395   */
396  public static String[] split(
397      String str, char escapeChar, char separator) {
398    if (str==null) {
399      return null;
400    }
401    ArrayList<String> strList = new ArrayList<String>();
402    StringBuilder split = new StringBuilder();
403    int index = 0;
404    while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
405      ++index; // move over the separator for next search
406      strList.add(split.toString());
407      split.setLength(0); // reset the buffer 
408    }
409    strList.add(split.toString());
410    // remove trailing empty split(s)
411    int last = strList.size(); // last split
412    while (--last>=0 && "".equals(strList.get(last))) {
413      strList.remove(last);
414    }
415    return strList.toArray(new String[strList.size()]);
416  }
417
418  /**
419   * Split a string using the given separator, with no escaping performed.
420   * @param str a string to be split. Note that this may not be null.
421   * @param separator a separator char
422   * @return an array of strings
423   */
424  public static String[] split(
425      String str, char separator) {
426    // String.split returns a single empty result for splitting the empty
427    // string.
428    if (str.isEmpty()) {
429      return new String[]{""};
430    }
431    ArrayList<String> strList = new ArrayList<String>();
432    int startIndex = 0;
433    int nextIndex = 0;
434    while ((nextIndex = str.indexOf(separator, startIndex)) != -1) {
435      strList.add(str.substring(startIndex, nextIndex));
436      startIndex = nextIndex + 1;
437    }
438    strList.add(str.substring(startIndex));
439    // remove trailing empty split(s)
440    int last = strList.size(); // last split
441    while (--last>=0 && "".equals(strList.get(last))) {
442      strList.remove(last);
443    }
444    return strList.toArray(new String[strList.size()]);
445  }
446  
447  /**
448   * Finds the first occurrence of the separator character ignoring the escaped
449   * separators starting from the index. Note the substring between the index
450   * and the position of the separator is passed.
451   * @param str the source string
452   * @param separator the character to find
453   * @param escapeChar character used to escape
454   * @param start from where to search
455   * @param split used to pass back the extracted string
456   */
457  public static int findNext(String str, char separator, char escapeChar, 
458                             int start, StringBuilder split) {
459    int numPreEscapes = 0;
460    for (int i = start; i < str.length(); i++) {
461      char curChar = str.charAt(i);
462      if (numPreEscapes == 0 && curChar == separator) { // separator 
463        return i;
464      } else {
465        split.append(curChar);
466        numPreEscapes = (curChar == escapeChar)
467                        ? (++numPreEscapes) % 2
468                        : 0;
469      }
470    }
471    return -1;
472  }
473  
474  /**
475   * Escape commas in the string using the default escape char
476   * @param str a string
477   * @return an escaped string
478   */
479  public static String escapeString(String str) {
480    return escapeString(str, ESCAPE_CHAR, COMMA);
481  }
482  
483  /**
484   * Escape <code>charToEscape</code> in the string 
485   * with the escape char <code>escapeChar</code>
486   * 
487   * @param str string
488   * @param escapeChar escape char
489   * @param charToEscape the char to be escaped
490   * @return an escaped string
491   */
492  public static String escapeString(
493      String str, char escapeChar, char charToEscape) {
494    return escapeString(str, escapeChar, new char[] {charToEscape});
495  }
496  
497  // check if the character array has the character 
498  private static boolean hasChar(char[] chars, char character) {
499    for (char target : chars) {
500      if (character == target) {
501        return true;
502      }
503    }
504    return false;
505  }
506  
507  /**
508   * @param charsToEscape array of characters to be escaped
509   */
510  public static String escapeString(String str, char escapeChar, 
511                                    char[] charsToEscape) {
512    if (str == null) {
513      return null;
514    }
515    StringBuilder result = new StringBuilder();
516    for (int i=0; i<str.length(); i++) {
517      char curChar = str.charAt(i);
518      if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
519        // special char
520        result.append(escapeChar);
521      }
522      result.append(curChar);
523    }
524    return result.toString();
525  }
526  
527  /**
528   * Unescape commas in the string using the default escape char
529   * @param str a string
530   * @return an unescaped string
531   */
532  public static String unEscapeString(String str) {
533    return unEscapeString(str, ESCAPE_CHAR, COMMA);
534  }
535  
536  /**
537   * Unescape <code>charToEscape</code> in the string 
538   * with the escape char <code>escapeChar</code>
539   * 
540   * @param str string
541   * @param escapeChar escape char
542   * @param charToEscape the escaped char
543   * @return an unescaped string
544   */
545  public static String unEscapeString(
546      String str, char escapeChar, char charToEscape) {
547    return unEscapeString(str, escapeChar, new char[] {charToEscape});
548  }
549  
550  /**
551   * @param charsToEscape array of characters to unescape
552   */
553  public static String unEscapeString(String str, char escapeChar, 
554                                      char[] charsToEscape) {
555    if (str == null) {
556      return null;
557    }
558    StringBuilder result = new StringBuilder(str.length());
559    boolean hasPreEscape = false;
560    for (int i=0; i<str.length(); i++) {
561      char curChar = str.charAt(i);
562      if (hasPreEscape) {
563        if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
564          // no special char
565          throw new IllegalArgumentException("Illegal escaped string " + str + 
566              " unescaped " + escapeChar + " at " + (i-1));
567        } 
568        // otherwise discard the escape char
569        result.append(curChar);
570        hasPreEscape = false;
571      } else {
572        if (hasChar(charsToEscape, curChar)) {
573          throw new IllegalArgumentException("Illegal escaped string " + str + 
574              " unescaped " + curChar + " at " + i);
575        } else if (curChar == escapeChar) {
576          hasPreEscape = true;
577        } else {
578          result.append(curChar);
579        }
580      }
581    }
582    if (hasPreEscape ) {
583      throw new IllegalArgumentException("Illegal escaped string " + str + 
584          ", not expecting " + escapeChar + " in the end." );
585    }
586    return result.toString();
587  }
588  
589  /**
590   * Return a message for logging.
591   * @param prefix prefix keyword for the message
592   * @param msg content of the message
593   * @return a message for logging
594   */
595  private static String toStartupShutdownString(String prefix, String [] msg) {
596    StringBuilder b = new StringBuilder(prefix);
597    b.append("\n/************************************************************");
598    for(String s : msg)
599      b.append("\n" + prefix + s);
600    b.append("\n************************************************************/");
601    return b.toString();
602  }
603
604  /**
605   * Print a log message for starting up and shutting down
606   * @param clazz the class of the server
607   * @param args arguments
608   * @param LOG the target log object
609   */
610  public static void startupShutdownMessage(Class<?> clazz, String[] args,
611                                     final org.apache.commons.logging.Log LOG) {
612    final String hostname = NetUtils.getHostname();
613    final String classname = clazz.getSimpleName();
614    LOG.info(
615        toStartupShutdownString("STARTUP_MSG: ", new String[] {
616            "Starting " + classname,
617            "  host = " + hostname,
618            "  args = " + Arrays.asList(args),
619            "  version = " + VersionInfo.getVersion(),
620            "  classpath = " + System.getProperty("java.class.path"),
621            "  build = " + VersionInfo.getUrl() + " -r "
622                         + VersionInfo.getRevision()  
623                         + "; compiled by '" + VersionInfo.getUser()
624                         + "' on " + VersionInfo.getDate(),
625            "  java = " + System.getProperty("java.version") }
626        )
627      );
628
629    if (SystemUtils.IS_OS_UNIX) {
630      try {
631        SignalLogger.INSTANCE.register(LOG);
632      } catch (Throwable t) {
633        LOG.warn("failed to register any UNIX signal loggers: ", t);
634      }
635    }
636    ShutdownHookManager.get().addShutdownHook(
637      new Runnable() {
638        @Override
639        public void run() {
640          LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
641            "Shutting down " + classname + " at " + hostname}));
642        }
643      }, SHUTDOWN_HOOK_PRIORITY);
644
645  }
646
647  /**
648   * The traditional binary prefixes, kilo, mega, ..., exa,
649   * which can be represented by a 64-bit integer.
650   * TraditionalBinaryPrefix symbol are case insensitive. 
651   */
652  public static enum TraditionalBinaryPrefix {
653    KILO(10),
654    MEGA(KILO.bitShift + 10),
655    GIGA(MEGA.bitShift + 10),
656    TERA(GIGA.bitShift + 10),
657    PETA(TERA.bitShift + 10),
658    EXA (PETA.bitShift + 10);
659
660    public final long value;
661    public final char symbol;
662    public final int bitShift;
663    public final long bitMask;
664
665    private TraditionalBinaryPrefix(int bitShift) {
666      this.bitShift = bitShift;
667      this.value = 1L << bitShift;
668      this.bitMask = this.value - 1L;
669      this.symbol = toString().charAt(0);
670    }
671
672    /**
673     * @return The TraditionalBinaryPrefix object corresponding to the symbol.
674     */
675    public static TraditionalBinaryPrefix valueOf(char symbol) {
676      symbol = Character.toUpperCase(symbol);
677      for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
678        if (symbol == prefix.symbol) {
679          return prefix;
680        }
681      }
682      throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
683    }
684
685    /**
686     * Convert a string to long.
687     * The input string is first be trimmed
688     * and then it is parsed with traditional binary prefix.
689     *
690     * For example,
691     * "-1230k" will be converted to -1230 * 1024 = -1259520;
692     * "891g" will be converted to 891 * 1024^3 = 956703965184;
693     *
694     * @param s input string
695     * @return a long value represented by the input string.
696     */
697    public static long string2long(String s) {
698      s = s.trim();
699      final int lastpos = s.length() - 1;
700      final char lastchar = s.charAt(lastpos);
701      if (Character.isDigit(lastchar))
702        return Long.parseLong(s);
703      else {
704        long prefix;
705        try {
706          prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
707        } catch (IllegalArgumentException e) {
708          throw new IllegalArgumentException("Invalid size prefix '" + lastchar
709              + "' in '" + s
710              + "'. Allowed prefixes are k, m, g, t, p, e(case insensitive)");
711        }
712        long num = Long.parseLong(s.substring(0, lastpos));
713        if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
714          throw new IllegalArgumentException(s + " does not fit in a Long");
715        }
716        return num * prefix;
717      }
718    }
719
720    /**
721     * Convert a long integer to a string with traditional binary prefix.
722     * 
723     * @param n the value to be converted
724     * @param unit The unit, e.g. "B" for bytes.
725     * @param decimalPlaces The number of decimal places.
726     * @return a string with traditional binary prefix.
727     */
728    public static String long2String(long n, String unit, int decimalPlaces) {
729      if (unit == null) {
730        unit = "";
731      }
732      //take care a special case
733      if (n == Long.MIN_VALUE) {
734        return "-8 " + EXA.symbol + unit;
735      }
736
737      final StringBuilder b = new StringBuilder();
738      //take care negative numbers
739      if (n < 0) {
740        b.append('-');
741        n = -n;
742      }
743      if (n < KILO.value) {
744        //no prefix
745        b.append(n);
746        return (unit.isEmpty()? b: b.append(" ").append(unit)).toString();
747      } else {
748        //find traditional binary prefix
749        int i = 0;
750        for(; i < values().length && n >= values()[i].value; i++);
751        TraditionalBinaryPrefix prefix = values()[i - 1];
752
753        if ((n & prefix.bitMask) == 0) {
754          //exact division
755          b.append(n >> prefix.bitShift);
756        } else {
757          final String  format = "%." + decimalPlaces + "f";
758          String s = format(format, n/(double)prefix.value);
759          //check a special rounding up case
760          if (s.startsWith("1024")) {
761            prefix = values()[i];
762            s = format(format, n/(double)prefix.value);
763          }
764          b.append(s);
765        }
766        return b.append(' ').append(prefix.symbol).append(unit).toString();
767      }
768    }
769  }
770
771    /**
772     * Escapes HTML Special characters present in the string.
773     * @param string
774     * @return HTML Escaped String representation
775     */
776    public static String escapeHTML(String string) {
777      if(string == null) {
778        return null;
779      }
780      StringBuilder sb = new StringBuilder();
781      boolean lastCharacterWasSpace = false;
782      char[] chars = string.toCharArray();
783      for(char c : chars) {
784        if(c == ' ') {
785          if(lastCharacterWasSpace){
786            lastCharacterWasSpace = false;
787            sb.append("&nbsp;");
788          }else {
789            lastCharacterWasSpace=true;
790            sb.append(" ");
791          }
792        }else {
793          lastCharacterWasSpace = false;
794          switch(c) {
795          case '<': sb.append("&lt;"); break;
796          case '>': sb.append("&gt;"); break;
797          case '&': sb.append("&amp;"); break;
798          case '"': sb.append("&quot;"); break;
799          default : sb.append(c);break;
800          }
801        }
802      }
803      
804      return sb.toString();
805    }
806
807  /**
808   * @return a byte description of the given long interger value.
809   */
810  public static String byteDesc(long len) {
811    return TraditionalBinaryPrefix.long2String(len, "B", 2);
812  }
813
814  /** @deprecated use StringUtils.format("%.2f", d). */
815  @Deprecated
816  public static String limitDecimalTo2(double d) {
817    return format("%.2f", d);
818  }
819  
820  /**
821   * Concatenates strings, using a separator.
822   *
823   * @param separator Separator to join with.
824   * @param strings Strings to join.
825   */
826  public static String join(CharSequence separator, Iterable<?> strings) {
827    Iterator<?> i = strings.iterator();
828    if (!i.hasNext()) {
829      return "";
830    }
831    StringBuilder sb = new StringBuilder(i.next().toString());
832    while (i.hasNext()) {
833      sb.append(separator);
834      sb.append(i.next().toString());
835    }
836    return sb.toString();
837  }
838
839  /**
840   * Concatenates strings, using a separator.
841   *
842   * @param separator to join with
843   * @param strings to join
844   * @return  the joined string
845   */
846  public static String join(CharSequence separator, String[] strings) {
847    // Ideally we don't have to duplicate the code here if array is iterable.
848    StringBuilder sb = new StringBuilder();
849    boolean first = true;
850    for (String s : strings) {
851      if (first) {
852        first = false;
853      } else {
854        sb.append(separator);
855      }
856      sb.append(s);
857    }
858    return sb.toString();
859  }
860
861  /**
862   * Convert SOME_STUFF to SomeStuff
863   *
864   * @param s input string
865   * @return camelized string
866   */
867  public static String camelize(String s) {
868    StringBuilder sb = new StringBuilder();
869    String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
870
871    for (String word : words)
872      sb.append(org.apache.commons.lang.StringUtils.capitalize(word));
873
874    return sb.toString();
875  }
876
877  /**
878   * Matches a template string against a pattern, replaces matched tokens with
879   * the supplied replacements, and returns the result.  The regular expression
880   * must use a capturing group.  The value of the first capturing group is used
881   * to look up the replacement.  If no replacement is found for the token, then
882   * it is replaced with the empty string.
883   * 
884   * For example, assume template is "%foo%_%bar%_%baz%", pattern is "%(.*?)%",
885   * and replacements contains 2 entries, mapping "foo" to "zoo" and "baz" to
886   * "zaz".  The result returned would be "zoo__zaz".
887   * 
888   * @param template String template to receive replacements
889   * @param pattern Pattern to match for identifying tokens, must use a capturing
890   *   group
891   * @param replacements Map<String, String> mapping tokens identified by the
892   *   capturing group to their replacement values
893   * @return String template with replacements
894   */
895  public static String replaceTokens(String template, Pattern pattern,
896      Map<String, String> replacements) {
897    StringBuffer sb = new StringBuffer();
898    Matcher matcher = pattern.matcher(template);
899    while (matcher.find()) {
900      String replacement = replacements.get(matcher.group(1));
901      if (replacement == null) {
902        replacement = "";
903      }
904      matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
905    }
906    matcher.appendTail(sb);
907    return sb.toString();
908  }
909  
910  /**
911   * Get stack trace for a given thread.
912   */
913  public static String getStackTrace(Thread t) {
914    final StackTraceElement[] stackTrace = t.getStackTrace();
915    StringBuilder str = new StringBuilder();
916    for (StackTraceElement e : stackTrace) {
917      str.append(e.toString() + "\n");
918    }
919    return str.toString();
920  }
921
922  /**
923   * From a list of command-line arguments, remove both an option and the 
924   * next argument.
925   *
926   * @param name  Name of the option to remove.  Example: -foo.
927   * @param args  List of arguments.
928   * @return      null if the option was not found; the value of the 
929   *              option otherwise.
930   * @throws IllegalArgumentException if the option's argument is not present
931   */
932  public static String popOptionWithArgument(String name, List<String> args)
933      throws IllegalArgumentException {
934    String val = null;
935    for (Iterator<String> iter = args.iterator(); iter.hasNext(); ) {
936      String cur = iter.next();
937      if (cur.equals("--")) {
938        // stop parsing arguments when you see --
939        break;
940      } else if (cur.equals(name)) {
941        iter.remove();
942        if (!iter.hasNext()) {
943          throw new IllegalArgumentException("option " + name + " requires 1 " +
944              "argument.");
945        }
946        val = iter.next();
947        iter.remove();
948        break;
949      }
950    }
951    return val;
952  }
953  
954  /**
955   * From a list of command-line arguments, remove an option.
956   *
957   * @param name  Name of the option to remove.  Example: -foo.
958   * @param args  List of arguments.
959   * @return      true if the option was found and removed; false otherwise.
960   */
961  public static boolean popOption(String name, List<String> args) {
962    for (Iterator<String> iter = args.iterator(); iter.hasNext(); ) {
963      String cur = iter.next();
964      if (cur.equals("--")) {
965        // stop parsing arguments when you see --
966        break;
967      } else if (cur.equals(name)) {
968        iter.remove();
969        return true;
970      }
971    }
972    return false;
973  }
974  
975  /**
976   * From a list of command-line arguments, return the first non-option
977   * argument.  Non-option arguments are those which either come after 
978   * a double dash (--) or do not start with a dash.
979   *
980   * @param args  List of arguments.
981   * @return      The first non-option argument, or null if there were none.
982   */
983  public static String popFirstNonOption(List<String> args) {
984    for (Iterator<String> iter = args.iterator(); iter.hasNext(); ) {
985      String cur = iter.next();
986      if (cur.equals("--")) {
987        if (!iter.hasNext()) {
988          return null;
989        }
990        cur = iter.next();
991        iter.remove();
992        return cur;
993      } else if (!cur.startsWith("-")) {
994        iter.remove();
995        return cur;
996      }
997    }
998    return null;
999  }
1000}