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