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    
019    package org.apache.hadoop.util;
020    
021    import java.io.PrintWriter;
022    import java.io.StringWriter;
023    import java.net.URI;
024    import java.net.URISyntaxException;
025    import java.text.DateFormat;
026    import java.util.ArrayList;
027    import java.util.Arrays;
028    import java.util.Collection;
029    import java.util.Date;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Locale;
033    import java.util.Map;
034    import java.util.StringTokenizer;
035    import java.util.regex.Matcher;
036    import java.util.regex.Pattern;
037    
038    import org.apache.commons.lang.SystemUtils;
039    import org.apache.hadoop.classification.InterfaceAudience;
040    import org.apache.hadoop.classification.InterfaceStability;
041    import org.apache.hadoop.fs.Path;
042    import org.apache.hadoop.net.NetUtils;
043    
044    import com.google.common.net.InetAddresses;
045    
046    /**
047     * General string utils
048     */
049    @InterfaceAudience.Private
050    @InterfaceStability.Unstable
051    public 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        values = new ArrayList<String>();
347        while (tokenizer.hasMoreTokens()) {
348          values.add(tokenizer.nextToken());
349        }
350        return values;
351      }
352    
353      /**
354       * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
355       * @param str a comma separated <String> with values
356       * @return a <code>Collection</code> of <code>String</code> values
357       */
358      public static Collection<String> getTrimmedStringCollection(String str){
359        return new ArrayList<String>(
360          Arrays.asList(getTrimmedStrings(str)));
361      }
362      
363      /**
364       * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
365       * @param str a comma separated <String> with values
366       * @return an array of <code>String</code> values
367       */
368      public static String[] getTrimmedStrings(String str){
369        if (null == str || str.trim().isEmpty()) {
370          return emptyStringArray;
371        }
372    
373        return str.trim().split("\\s*,\\s*");
374      }
375    
376      final public static String[] emptyStringArray = {};
377      final public static char COMMA = ',';
378      final public static String COMMA_STR = ",";
379      final public static char ESCAPE_CHAR = '\\';
380      
381      /**
382       * Split a string using the default separator
383       * @param str a string that may have escaped separator
384       * @return an array of strings
385       */
386      public static String[] split(String str) {
387        return split(str, ESCAPE_CHAR, COMMA);
388      }
389      
390      /**
391       * Split a string using the given separator
392       * @param str a string that may have escaped separator
393       * @param escapeChar a char that be used to escape the separator
394       * @param separator a separator char
395       * @return an array of strings
396       */
397      public static String[] split(
398          String str, char escapeChar, char separator) {
399        if (str==null) {
400          return null;
401        }
402        ArrayList<String> strList = new ArrayList<String>();
403        StringBuilder split = new StringBuilder();
404        int index = 0;
405        while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
406          ++index; // move over the separator for next search
407          strList.add(split.toString());
408          split.setLength(0); // reset the buffer 
409        }
410        strList.add(split.toString());
411        // remove trailing empty split(s)
412        int last = strList.size(); // last split
413        while (--last>=0 && "".equals(strList.get(last))) {
414          strList.remove(last);
415        }
416        return strList.toArray(new String[strList.size()]);
417      }
418    
419      /**
420       * Split a string using the given separator, with no escaping performed.
421       * @param str a string to be split. Note that this may not be null.
422       * @param separator a separator char
423       * @return an array of strings
424       */
425      public static String[] split(
426          String str, char separator) {
427        // String.split returns a single empty result for splitting the empty
428        // string.
429        if (str.isEmpty()) {
430          return new String[]{""};
431        }
432        ArrayList<String> strList = new ArrayList<String>();
433        int startIndex = 0;
434        int nextIndex = 0;
435        while ((nextIndex = str.indexOf((int)separator, startIndex)) != -1) {
436          strList.add(str.substring(startIndex, nextIndex));
437          startIndex = nextIndex + 1;
438        }
439        strList.add(str.substring(startIndex));
440        // remove trailing empty split(s)
441        int last = strList.size(); // last split
442        while (--last>=0 && "".equals(strList.get(last))) {
443          strList.remove(last);
444        }
445        return strList.toArray(new String[strList.size()]);
446      }
447      
448      /**
449       * Finds the first occurrence of the separator character ignoring the escaped
450       * separators starting from the index. Note the substring between the index
451       * and the position of the separator is passed.
452       * @param str the source string
453       * @param separator the character to find
454       * @param escapeChar character used to escape
455       * @param start from where to search
456       * @param split used to pass back the extracted string
457       */
458      public static int findNext(String str, char separator, char escapeChar, 
459                                 int start, StringBuilder split) {
460        int numPreEscapes = 0;
461        for (int i = start; i < str.length(); i++) {
462          char curChar = str.charAt(i);
463          if (numPreEscapes == 0 && curChar == separator) { // separator 
464            return i;
465          } else {
466            split.append(curChar);
467            numPreEscapes = (curChar == escapeChar)
468                            ? (++numPreEscapes) % 2
469                            : 0;
470          }
471        }
472        return -1;
473      }
474      
475      /**
476       * Escape commas in the string using the default escape char
477       * @param str a string
478       * @return an escaped string
479       */
480      public static String escapeString(String str) {
481        return escapeString(str, ESCAPE_CHAR, COMMA);
482      }
483      
484      /**
485       * Escape <code>charToEscape</code> in the string 
486       * with the escape char <code>escapeChar</code>
487       * 
488       * @param str string
489       * @param escapeChar escape char
490       * @param charToEscape the char to be escaped
491       * @return an escaped string
492       */
493      public static String escapeString(
494          String str, char escapeChar, char charToEscape) {
495        return escapeString(str, escapeChar, new char[] {charToEscape});
496      }
497      
498      // check if the character array has the character 
499      private static boolean hasChar(char[] chars, char character) {
500        for (char target : chars) {
501          if (character == target) {
502            return true;
503          }
504        }
505        return false;
506      }
507      
508      /**
509       * @param charsToEscape array of characters to be escaped
510       */
511      public static String escapeString(String str, char escapeChar, 
512                                        char[] charsToEscape) {
513        if (str == null) {
514          return null;
515        }
516        StringBuilder result = new StringBuilder();
517        for (int i=0; i<str.length(); i++) {
518          char curChar = str.charAt(i);
519          if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
520            // special char
521            result.append(escapeChar);
522          }
523          result.append(curChar);
524        }
525        return result.toString();
526      }
527      
528      /**
529       * Unescape commas in the string using the default escape char
530       * @param str a string
531       * @return an unescaped string
532       */
533      public static String unEscapeString(String str) {
534        return unEscapeString(str, ESCAPE_CHAR, COMMA);
535      }
536      
537      /**
538       * Unescape <code>charToEscape</code> in the string 
539       * with the escape char <code>escapeChar</code>
540       * 
541       * @param str string
542       * @param escapeChar escape char
543       * @param charToEscape the escaped char
544       * @return an unescaped string
545       */
546      public static String unEscapeString(
547          String str, char escapeChar, char charToEscape) {
548        return unEscapeString(str, escapeChar, new char[] {charToEscape});
549      }
550      
551      /**
552       * @param charsToEscape array of characters to unescape
553       */
554      public static String unEscapeString(String str, char escapeChar, 
555                                          char[] charsToEscape) {
556        if (str == null) {
557          return null;
558        }
559        StringBuilder result = new StringBuilder(str.length());
560        boolean hasPreEscape = false;
561        for (int i=0; i<str.length(); i++) {
562          char curChar = str.charAt(i);
563          if (hasPreEscape) {
564            if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
565              // no special char
566              throw new IllegalArgumentException("Illegal escaped string " + str + 
567                  " unescaped " + escapeChar + " at " + (i-1));
568            } 
569            // otherwise discard the escape char
570            result.append(curChar);
571            hasPreEscape = false;
572          } else {
573            if (hasChar(charsToEscape, curChar)) {
574              throw new IllegalArgumentException("Illegal escaped string " + str + 
575                  " unescaped " + curChar + " at " + i);
576            } else if (curChar == escapeChar) {
577              hasPreEscape = true;
578            } else {
579              result.append(curChar);
580            }
581          }
582        }
583        if (hasPreEscape ) {
584          throw new IllegalArgumentException("Illegal escaped string " + str + 
585              ", not expecting " + escapeChar + " in the end." );
586        }
587        return result.toString();
588      }
589      
590      /**
591       * Return a message for logging.
592       * @param prefix prefix keyword for the message
593       * @param msg content of the message
594       * @return a message for logging
595       */
596      private static String toStartupShutdownString(String prefix, String [] msg) {
597        StringBuilder b = new StringBuilder(prefix);
598        b.append("\n/************************************************************");
599        for(String s : msg)
600          b.append("\n" + prefix + s);
601        b.append("\n************************************************************/");
602        return b.toString();
603      }
604    
605      /**
606       * Print a log message for starting up and shutting down
607       * @param clazz the class of the server
608       * @param args arguments
609       * @param LOG the target log object
610       */
611      public static void startupShutdownMessage(Class<?> clazz, String[] args,
612                                         final org.apache.commons.logging.Log LOG) {
613        final String hostname = NetUtils.getHostname();
614        final String classname = clazz.getSimpleName();
615        LOG.info(
616            toStartupShutdownString("STARTUP_MSG: ", new String[] {
617                "Starting " + classname,
618                "  host = " + hostname,
619                "  args = " + Arrays.asList(args),
620                "  version = " + VersionInfo.getVersion(),
621                "  classpath = " + System.getProperty("java.class.path"),
622                "  build = " + VersionInfo.getUrl() + " -r "
623                             + VersionInfo.getRevision()  
624                             + "; compiled by '" + VersionInfo.getUser()
625                             + "' on " + VersionInfo.getDate(),
626                "  java = " + System.getProperty("java.version") }
627            )
628          );
629    
630        if (SystemUtils.IS_OS_UNIX) {
631          try {
632            SignalLogger.INSTANCE.register(LOG);
633          } catch (Throwable t) {
634            LOG.warn("failed to register any UNIX signal loggers: ", t);
635          }
636        }
637        ShutdownHookManager.get().addShutdownHook(
638          new Runnable() {
639            @Override
640            public void run() {
641              LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
642                "Shutting down " + classname + " at " + hostname}));
643            }
644          }, SHUTDOWN_HOOK_PRIORITY);
645    
646      }
647    
648      /**
649       * The traditional binary prefixes, kilo, mega, ..., exa,
650       * which can be represented by a 64-bit integer.
651       * TraditionalBinaryPrefix symbol are case insensitive. 
652       */
653      public static enum TraditionalBinaryPrefix {
654        KILO(10),
655        MEGA(KILO.bitShift + 10),
656        GIGA(MEGA.bitShift + 10),
657        TERA(GIGA.bitShift + 10),
658        PETA(TERA.bitShift + 10),
659        EXA (PETA.bitShift + 10);
660    
661        public final long value;
662        public final char symbol;
663        public final int bitShift;
664        public final long bitMask;
665    
666        private TraditionalBinaryPrefix(int bitShift) {
667          this.bitShift = bitShift;
668          this.value = 1L << bitShift;
669          this.bitMask = this.value - 1L;
670          this.symbol = toString().charAt(0);
671        }
672    
673        /**
674         * @return The TraditionalBinaryPrefix object corresponding to the symbol.
675         */
676        public static TraditionalBinaryPrefix valueOf(char symbol) {
677          symbol = Character.toUpperCase(symbol);
678          for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
679            if (symbol == prefix.symbol) {
680              return prefix;
681            }
682          }
683          throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
684        }
685    
686        /**
687         * Convert a string to long.
688         * The input string is first be trimmed
689         * and then it is parsed with traditional binary prefix.
690         *
691         * For example,
692         * "-1230k" will be converted to -1230 * 1024 = -1259520;
693         * "891g" will be converted to 891 * 1024^3 = 956703965184;
694         *
695         * @param s input string
696         * @return a long value represented by the input string.
697         */
698        public static long string2long(String s) {
699          s = s.trim();
700          final int lastpos = s.length() - 1;
701          final char lastchar = s.charAt(lastpos);
702          if (Character.isDigit(lastchar))
703            return Long.parseLong(s);
704          else {
705            long prefix;
706            try {
707              prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
708            } catch (IllegalArgumentException e) {
709              throw new IllegalArgumentException("Invalid size prefix '" + lastchar
710                  + "' in '" + s
711                  + "'. Allowed prefixes are k, m, g, t, p, e(case insensitive)");
712            }
713            long num = Long.parseLong(s.substring(0, lastpos));
714            if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
715              throw new IllegalArgumentException(s + " does not fit in a Long");
716            }
717            return num * prefix;
718          }
719        }
720    
721        /**
722         * Convert a long integer to a string with traditional binary prefix.
723         * 
724         * @param n the value to be converted
725         * @param unit The unit, e.g. "B" for bytes.
726         * @param decimalPlaces The number of decimal places.
727         * @return a string with traditional binary prefix.
728         */
729        public static String long2String(long n, String unit, int decimalPlaces) {
730          if (unit == null) {
731            unit = "";
732          }
733          //take care a special case
734          if (n == Long.MIN_VALUE) {
735            return "-8 " + EXA.symbol + unit;
736          }
737    
738          final StringBuilder b = new StringBuilder();
739          //take care negative numbers
740          if (n < 0) {
741            b.append('-');
742            n = -n;
743          }
744          if (n < KILO.value) {
745            //no prefix
746            b.append(n);
747            return (unit.isEmpty()? b: b.append(" ").append(unit)).toString();
748          } else {
749            //find traditional binary prefix
750            int i = 0;
751            for(; i < values().length && n >= values()[i].value; i++);
752            TraditionalBinaryPrefix prefix = values()[i - 1];
753    
754            if ((n & prefix.bitMask) == 0) {
755              //exact division
756              b.append(n >> prefix.bitShift);
757            } else {
758              final String  format = "%." + decimalPlaces + "f";
759              String s = format(format, n/(double)prefix.value);
760              //check a special rounding up case
761              if (s.startsWith("1024")) {
762                prefix = values()[i];
763                s = format(format, n/(double)prefix.value);
764              }
765              b.append(s);
766            }
767            return b.append(' ').append(prefix.symbol).append(unit).toString();
768          }
769        }
770      }
771    
772        /**
773         * Escapes HTML Special characters present in the string.
774         * @param string
775         * @return HTML Escaped String representation
776         */
777        public static String escapeHTML(String string) {
778          if(string == null) {
779            return null;
780          }
781          StringBuilder sb = new StringBuilder();
782          boolean lastCharacterWasSpace = false;
783          char[] chars = string.toCharArray();
784          for(char c : chars) {
785            if(c == ' ') {
786              if(lastCharacterWasSpace){
787                lastCharacterWasSpace = false;
788                sb.append("&nbsp;");
789              }else {
790                lastCharacterWasSpace=true;
791                sb.append(" ");
792              }
793            }else {
794              lastCharacterWasSpace = false;
795              switch(c) {
796              case '<': sb.append("&lt;"); break;
797              case '>': sb.append("&gt;"); break;
798              case '&': sb.append("&amp;"); break;
799              case '"': sb.append("&quot;"); break;
800              default : sb.append(c);break;
801              }
802            }
803          }
804          
805          return sb.toString();
806        }
807    
808      /**
809       * @return a byte description of the given long interger value.
810       */
811      public static String byteDesc(long len) {
812        return TraditionalBinaryPrefix.long2String(len, "B", 2);
813      }
814    
815      /** @deprecated use StringUtils.format("%.2f", d). */
816      @Deprecated
817      public static String limitDecimalTo2(double d) {
818        return format("%.2f", d);
819      }
820      
821      /**
822       * Concatenates strings, using a separator.
823       *
824       * @param separator Separator to join with.
825       * @param strings Strings to join.
826       */
827      public static String join(CharSequence separator, Iterable<?> strings) {
828        Iterator<?> i = strings.iterator();
829        if (!i.hasNext()) {
830          return "";
831        }
832        StringBuilder sb = new StringBuilder(i.next().toString());
833        while (i.hasNext()) {
834          sb.append(separator);
835          sb.append(i.next().toString());
836        }
837        return sb.toString();
838      }
839    
840      /**
841       * Concatenates strings, using a separator.
842       *
843       * @param separator to join with
844       * @param strings to join
845       * @return  the joined string
846       */
847      public static String join(CharSequence separator, String[] strings) {
848        // Ideally we don't have to duplicate the code here if array is iterable.
849        StringBuilder sb = new StringBuilder();
850        boolean first = true;
851        for (String s : strings) {
852          if (first) {
853            first = false;
854          } else {
855            sb.append(separator);
856          }
857          sb.append(s);
858        }
859        return sb.toString();
860      }
861    
862      /**
863       * Convert SOME_STUFF to SomeStuff
864       *
865       * @param s input string
866       * @return camelized string
867       */
868      public static String camelize(String s) {
869        StringBuilder sb = new StringBuilder();
870        String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
871    
872        for (String word : words)
873          sb.append(org.apache.commons.lang.StringUtils.capitalize(word));
874    
875        return sb.toString();
876      }
877    
878      /**
879       * Matches a template string against a pattern, replaces matched tokens with
880       * the supplied replacements, and returns the result.  The regular expression
881       * must use a capturing group.  The value of the first capturing group is used
882       * to look up the replacement.  If no replacement is found for the token, then
883       * it is replaced with the empty string.
884       * 
885       * For example, assume template is "%foo%_%bar%_%baz%", pattern is "%(.*?)%",
886       * and replacements contains 2 entries, mapping "foo" to "zoo" and "baz" to
887       * "zaz".  The result returned would be "zoo__zaz".
888       * 
889       * @param template String template to receive replacements
890       * @param pattern Pattern to match for identifying tokens, must use a capturing
891       *   group
892       * @param replacements Map<String, String> mapping tokens identified by the
893       *   capturing group to their replacement values
894       * @return String template with replacements
895       */
896      public static String replaceTokens(String template, Pattern pattern,
897          Map<String, String> replacements) {
898        StringBuffer sb = new StringBuffer();
899        Matcher matcher = pattern.matcher(template);
900        while (matcher.find()) {
901          String replacement = replacements.get(matcher.group(1));
902          if (replacement == null) {
903            replacement = "";
904          }
905          matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
906        }
907        matcher.appendTail(sb);
908        return sb.toString();
909      }
910      
911      /**
912       * Get stack trace for a given thread.
913       */
914      public static String getStackTrace(Thread t) {
915        final StackTraceElement[] stackTrace = t.getStackTrace();
916        StringBuilder str = new StringBuilder();
917        for (StackTraceElement e : stackTrace) {
918          str.append(e.toString() + "\n");
919        }
920        return str.toString();
921      }
922    }