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.text.DecimalFormat;
027import java.text.NumberFormat;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.Date;
032import java.util.Iterator;
033import java.util.List;
034import java.util.Locale;
035import java.util.StringTokenizer;
036
037import org.apache.hadoop.classification.InterfaceAudience;
038import org.apache.hadoop.classification.InterfaceStability;
039import org.apache.hadoop.fs.Path;
040import org.apache.hadoop.net.NetUtils;
041
042/**
043 * General string utils
044 */
045@InterfaceAudience.Private
046@InterfaceStability.Unstable
047public class StringUtils {
048
049  /**
050   * Priority of the StringUtils shutdown hook.
051   */
052  public static final int SHUTDOWN_HOOK_PRIORITY = 0;
053
054  private static final DecimalFormat decimalFormat;
055  static {
056          NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.ENGLISH);
057          decimalFormat = (DecimalFormat) numberFormat;
058          decimalFormat.applyPattern("#.##");
059  }
060
061  /**
062   * Make a string representation of the exception.
063   * @param e The exception to stringify
064   * @return A string with exception name and call stack.
065   */
066  public static String stringifyException(Throwable e) {
067    StringWriter stm = new StringWriter();
068    PrintWriter wrt = new PrintWriter(stm);
069    e.printStackTrace(wrt);
070    wrt.close();
071    return stm.toString();
072  }
073  
074  /**
075   * Given a full hostname, return the word upto the first dot.
076   * @param fullHostname the full hostname
077   * @return the hostname to the first dot
078   */
079  public static String simpleHostname(String fullHostname) {
080    int offset = fullHostname.indexOf('.');
081    if (offset != -1) {
082      return fullHostname.substring(0, offset);
083    }
084    return fullHostname;
085  }
086
087  private static DecimalFormat oneDecimal = new DecimalFormat("0.0");
088  
089  /**
090   * Given an integer, return a string that is in an approximate, but human 
091   * readable format. 
092   * It uses the bases 'k', 'm', and 'g' for 1024, 1024**2, and 1024**3.
093   * @param number the number to format
094   * @return a human readable form of the integer
095   */
096  public static String humanReadableInt(long number) {
097    long absNumber = Math.abs(number);
098    double result = number;
099    String suffix = "";
100    if (absNumber < 1024) {
101      // since no division has occurred, don't format with a decimal point
102      return String.valueOf(number);
103    } else if (absNumber < 1024 * 1024) {
104      result = number / 1024.0;
105      suffix = "k";
106    } else if (absNumber < 1024 * 1024 * 1024) {
107      result = number / (1024.0 * 1024);
108      suffix = "m";
109    } else {
110      result = number / (1024.0 * 1024 * 1024);
111      suffix = "g";
112    }
113    return oneDecimal.format(result) + suffix;
114  }
115  
116  /**
117   * Format a percentage for presentation to the user.
118   * @param done the percentage to format (0.0 to 1.0)
119   * @param digits the number of digits past the decimal point
120   * @return a string representation of the percentage
121   */
122  public static String formatPercent(double done, int digits) {
123    DecimalFormat percentFormat = new DecimalFormat("0.00%");
124    double scale = Math.pow(10.0, digits+2);
125    double rounded = Math.floor(done * scale);
126    percentFormat.setDecimalSeparatorAlwaysShown(false);
127    percentFormat.setMinimumFractionDigits(digits);
128    percentFormat.setMaximumFractionDigits(digits);
129    return percentFormat.format(rounded / scale);
130  }
131  
132  /**
133   * Given an array of strings, return a comma-separated list of its elements.
134   * @param strs Array of strings
135   * @return Empty string if strs.length is 0, comma separated list of strings
136   * otherwise
137   */
138  
139  public static String arrayToString(String[] strs) {
140    if (strs.length == 0) { return ""; }
141    StringBuilder sbuf = new StringBuilder();
142    sbuf.append(strs[0]);
143    for (int idx = 1; idx < strs.length; idx++) {
144      sbuf.append(",");
145      sbuf.append(strs[idx]);
146    }
147    return sbuf.toString();
148  }
149
150  /**
151   * Given an array of bytes it will convert the bytes to a hex string
152   * representation of the bytes
153   * @param bytes
154   * @param start start index, inclusively
155   * @param end end index, exclusively
156   * @return hex string representation of the byte array
157   */
158  public static String byteToHexString(byte[] bytes, int start, int end) {
159    if (bytes == null) {
160      throw new IllegalArgumentException("bytes == null");
161    }
162    StringBuilder s = new StringBuilder(); 
163    for(int i = start; i < end; i++) {
164      s.append(String.format("%02x", bytes[i]));
165    }
166    return s.toString();
167  }
168
169  /** Same as byteToHexString(bytes, 0, bytes.length). */
170  public static String byteToHexString(byte bytes[]) {
171    return byteToHexString(bytes, 0, bytes.length);
172  }
173
174  /**
175   * Given a hexstring this will return the byte array corresponding to the
176   * string
177   * @param hex the hex String array
178   * @return a byte array that is a hex string representation of the given
179   *         string. The size of the byte array is therefore hex.length/2
180   */
181  public static byte[] hexStringToByte(String hex) {
182    byte[] bts = new byte[hex.length() / 2];
183    for (int i = 0; i < bts.length; i++) {
184      bts[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
185    }
186    return bts;
187  }
188  /**
189   * 
190   * @param uris
191   */
192  public static String uriToString(URI[] uris){
193    if (uris == null) {
194      return null;
195    }
196    StringBuilder ret = new StringBuilder(uris[0].toString());
197    for(int i = 1; i < uris.length;i++){
198      ret.append(",");
199      ret.append(uris[i].toString());
200    }
201    return ret.toString();
202  }
203  
204  /**
205   * @param str
206   *          The string array to be parsed into an URI array.
207   * @return <tt>null</tt> if str is <tt>null</tt>, else the URI array
208   *         equivalent to str.
209   * @throws IllegalArgumentException
210   *           If any string in str violates RFC&nbsp;2396.
211   */
212  public static URI[] stringToURI(String[] str){
213    if (str == null) 
214      return null;
215    URI[] uris = new URI[str.length];
216    for (int i = 0; i < str.length;i++){
217      try{
218        uris[i] = new URI(str[i]);
219      }catch(URISyntaxException ur){
220        throw new IllegalArgumentException(
221            "Failed to create uri for " + str[i], ur);
222      }
223    }
224    return uris;
225  }
226  
227  /**
228   * 
229   * @param str
230   */
231  public static Path[] stringToPath(String[] str){
232    if (str == null) {
233      return null;
234    }
235    Path[] p = new Path[str.length];
236    for (int i = 0; i < str.length;i++){
237      p[i] = new Path(str[i]);
238    }
239    return p;
240  }
241  /**
242   * 
243   * Given a finish and start time in long milliseconds, returns a 
244   * String in the format Xhrs, Ymins, Z sec, for the time difference between two times. 
245   * If finish time comes before start time then negative valeus of X, Y and Z wil return. 
246   * 
247   * @param finishTime finish time
248   * @param startTime start time
249   */
250  public static String formatTimeDiff(long finishTime, long startTime){
251    long timeDiff = finishTime - startTime; 
252    return formatTime(timeDiff); 
253  }
254  
255  /**
256   * 
257   * Given the time in long milliseconds, returns a 
258   * String in the format Xhrs, Ymins, Z sec. 
259   * 
260   * @param timeDiff The time difference to format
261   */
262  public static String formatTime(long timeDiff){
263    StringBuilder buf = new StringBuilder();
264    long hours = timeDiff / (60*60*1000);
265    long rem = (timeDiff % (60*60*1000));
266    long minutes =  rem / (60*1000);
267    rem = rem % (60*1000);
268    long seconds = rem / 1000;
269    
270    if (hours != 0){
271      buf.append(hours);
272      buf.append("hrs, ");
273    }
274    if (minutes != 0){
275      buf.append(minutes);
276      buf.append("mins, ");
277    }
278    // return "0sec if no difference
279    buf.append(seconds);
280    buf.append("sec");
281    return buf.toString(); 
282  }
283  /**
284   * Formats time in ms and appends difference (finishTime - startTime) 
285   * as returned by formatTimeDiff().
286   * If finish time is 0, empty string is returned, if start time is 0 
287   * then difference is not appended to return value. 
288   * @param dateFormat date format to use
289   * @param finishTime fnish time
290   * @param startTime start time
291   * @return formatted value. 
292   */
293  public static String getFormattedTimeWithDiff(DateFormat dateFormat, 
294                                                long finishTime, long startTime){
295    StringBuilder buf = new StringBuilder();
296    if (0 != finishTime) {
297      buf.append(dateFormat.format(new Date(finishTime)));
298      if (0 != startTime){
299        buf.append(" (" + formatTimeDiff(finishTime , startTime) + ")");
300      }
301    }
302    return buf.toString();
303  }
304  
305  /**
306   * Returns an arraylist of strings.
307   * @param str the comma seperated string values
308   * @return the arraylist of the comma seperated string values
309   */
310  public static String[] getStrings(String str){
311    Collection<String> values = getStringCollection(str);
312    if(values.size() == 0) {
313      return null;
314    }
315    return values.toArray(new String[values.size()]);
316  }
317
318  /**
319   * Returns a collection of strings.
320   * @param str comma seperated string values
321   * @return an <code>ArrayList</code> of string values
322   */
323  public static Collection<String> getStringCollection(String str){
324    List<String> values = new ArrayList<String>();
325    if (str == null)
326      return values;
327    StringTokenizer tokenizer = new StringTokenizer (str,",");
328    values = new ArrayList<String>();
329    while (tokenizer.hasMoreTokens()) {
330      values.add(tokenizer.nextToken());
331    }
332    return values;
333  }
334
335  /**
336   * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
337   * @param str a comma separated <String> with values
338   * @return a <code>Collection</code> of <code>String</code> values
339   */
340  public static Collection<String> getTrimmedStringCollection(String str){
341    return new ArrayList<String>(
342      Arrays.asList(getTrimmedStrings(str)));
343  }
344  
345  /**
346   * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
347   * @param str a comma separated <String> with values
348   * @return an array of <code>String</code> values
349   */
350  public static String[] getTrimmedStrings(String str){
351    if (null == str || "".equals(str.trim())) {
352      return emptyStringArray;
353    }
354
355    return str.trim().split("\\s*,\\s*");
356  }
357
358  final public static String[] emptyStringArray = {};
359  final public static char COMMA = ',';
360  final public static String COMMA_STR = ",";
361  final public static char ESCAPE_CHAR = '\\';
362  
363  /**
364   * Split a string using the default separator
365   * @param str a string that may have escaped separator
366   * @return an array of strings
367   */
368  public static String[] split(String str) {
369    return split(str, ESCAPE_CHAR, COMMA);
370  }
371  
372  /**
373   * Split a string using the given separator
374   * @param str a string that may have escaped separator
375   * @param escapeChar a char that be used to escape the separator
376   * @param separator a separator char
377   * @return an array of strings
378   */
379  public static String[] split(
380      String str, char escapeChar, char separator) {
381    if (str==null) {
382      return null;
383    }
384    ArrayList<String> strList = new ArrayList<String>();
385    StringBuilder split = new StringBuilder();
386    int index = 0;
387    while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
388      ++index; // move over the separator for next search
389      strList.add(split.toString());
390      split.setLength(0); // reset the buffer 
391    }
392    strList.add(split.toString());
393    // remove trailing empty split(s)
394    int last = strList.size(); // last split
395    while (--last>=0 && "".equals(strList.get(last))) {
396      strList.remove(last);
397    }
398    return strList.toArray(new String[strList.size()]);
399  }
400
401  /**
402   * Split a string using the given separator, with no escaping performed.
403   * @param str a string to be split. Note that this may not be null.
404   * @param separator a separator char
405   * @return an array of strings
406   */
407  public static String[] split(
408      String str, char separator) {
409    // String.split returns a single empty result for splitting the empty
410    // string.
411    if ("".equals(str)) {
412      return new String[]{""};
413    }
414    ArrayList<String> strList = new ArrayList<String>();
415    int startIndex = 0;
416    int nextIndex = 0;
417    while ((nextIndex = str.indexOf((int)separator, startIndex)) != -1) {
418      strList.add(str.substring(startIndex, nextIndex));
419      startIndex = nextIndex + 1;
420    }
421    strList.add(str.substring(startIndex));
422    // remove trailing empty split(s)
423    int last = strList.size(); // last split
424    while (--last>=0 && "".equals(strList.get(last))) {
425      strList.remove(last);
426    }
427    return strList.toArray(new String[strList.size()]);
428  }
429  
430  /**
431   * Finds the first occurrence of the separator character ignoring the escaped
432   * separators starting from the index. Note the substring between the index
433   * and the position of the separator is passed.
434   * @param str the source string
435   * @param separator the character to find
436   * @param escapeChar character used to escape
437   * @param start from where to search
438   * @param split used to pass back the extracted string
439   */
440  public static int findNext(String str, char separator, char escapeChar, 
441                             int start, StringBuilder split) {
442    int numPreEscapes = 0;
443    for (int i = start; i < str.length(); i++) {
444      char curChar = str.charAt(i);
445      if (numPreEscapes == 0 && curChar == separator) { // separator 
446        return i;
447      } else {
448        split.append(curChar);
449        numPreEscapes = (curChar == escapeChar)
450                        ? (++numPreEscapes) % 2
451                        : 0;
452      }
453    }
454    return -1;
455  }
456  
457  /**
458   * Escape commas in the string using the default escape char
459   * @param str a string
460   * @return an escaped string
461   */
462  public static String escapeString(String str) {
463    return escapeString(str, ESCAPE_CHAR, COMMA);
464  }
465  
466  /**
467   * Escape <code>charToEscape</code> in the string 
468   * with the escape char <code>escapeChar</code>
469   * 
470   * @param str string
471   * @param escapeChar escape char
472   * @param charToEscape the char to be escaped
473   * @return an escaped string
474   */
475  public static String escapeString(
476      String str, char escapeChar, char charToEscape) {
477    return escapeString(str, escapeChar, new char[] {charToEscape});
478  }
479  
480  // check if the character array has the character 
481  private static boolean hasChar(char[] chars, char character) {
482    for (char target : chars) {
483      if (character == target) {
484        return true;
485      }
486    }
487    return false;
488  }
489  
490  /**
491   * @param charsToEscape array of characters to be escaped
492   */
493  public static String escapeString(String str, char escapeChar, 
494                                    char[] charsToEscape) {
495    if (str == null) {
496      return null;
497    }
498    StringBuilder result = new StringBuilder();
499    for (int i=0; i<str.length(); i++) {
500      char curChar = str.charAt(i);
501      if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
502        // special char
503        result.append(escapeChar);
504      }
505      result.append(curChar);
506    }
507    return result.toString();
508  }
509  
510  /**
511   * Unescape commas in the string using the default escape char
512   * @param str a string
513   * @return an unescaped string
514   */
515  public static String unEscapeString(String str) {
516    return unEscapeString(str, ESCAPE_CHAR, COMMA);
517  }
518  
519  /**
520   * Unescape <code>charToEscape</code> in the string 
521   * with the escape char <code>escapeChar</code>
522   * 
523   * @param str string
524   * @param escapeChar escape char
525   * @param charToEscape the escaped char
526   * @return an unescaped string
527   */
528  public static String unEscapeString(
529      String str, char escapeChar, char charToEscape) {
530    return unEscapeString(str, escapeChar, new char[] {charToEscape});
531  }
532  
533  /**
534   * @param charsToEscape array of characters to unescape
535   */
536  public static String unEscapeString(String str, char escapeChar, 
537                                      char[] charsToEscape) {
538    if (str == null) {
539      return null;
540    }
541    StringBuilder result = new StringBuilder(str.length());
542    boolean hasPreEscape = false;
543    for (int i=0; i<str.length(); i++) {
544      char curChar = str.charAt(i);
545      if (hasPreEscape) {
546        if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
547          // no special char
548          throw new IllegalArgumentException("Illegal escaped string " + str + 
549              " unescaped " + escapeChar + " at " + (i-1));
550        } 
551        // otherwise discard the escape char
552        result.append(curChar);
553        hasPreEscape = false;
554      } else {
555        if (hasChar(charsToEscape, curChar)) {
556          throw new IllegalArgumentException("Illegal escaped string " + str + 
557              " unescaped " + curChar + " at " + i);
558        } else if (curChar == escapeChar) {
559          hasPreEscape = true;
560        } else {
561          result.append(curChar);
562        }
563      }
564    }
565    if (hasPreEscape ) {
566      throw new IllegalArgumentException("Illegal escaped string " + str + 
567          ", not expecting " + escapeChar + " in the end." );
568    }
569    return result.toString();
570  }
571  
572  /**
573   * Return a message for logging.
574   * @param prefix prefix keyword for the message
575   * @param msg content of the message
576   * @return a message for logging
577   */
578  private static String toStartupShutdownString(String prefix, String [] msg) {
579    StringBuilder b = new StringBuilder(prefix);
580    b.append("\n/************************************************************");
581    for(String s : msg)
582      b.append("\n" + prefix + s);
583    b.append("\n************************************************************/");
584    return b.toString();
585  }
586
587  /**
588   * Print a log message for starting up and shutting down
589   * @param clazz the class of the server
590   * @param args arguments
591   * @param LOG the target log object
592   */
593  public static void startupShutdownMessage(Class<?> clazz, String[] args,
594                                     final org.apache.commons.logging.Log LOG) {
595    final String hostname = NetUtils.getHostname();
596    final String classname = clazz.getSimpleName();
597    LOG.info(
598        toStartupShutdownString("STARTUP_MSG: ", new String[] {
599            "Starting " + classname,
600            "  host = " + hostname,
601            "  args = " + Arrays.asList(args),
602            "  version = " + VersionInfo.getVersion(),
603            "  classpath = " + System.getProperty("java.class.path"),
604            "  build = " + VersionInfo.getUrl() + " -r "
605                         + VersionInfo.getRevision()  
606                         + "; compiled by '" + VersionInfo.getUser()
607                         + "' on " + VersionInfo.getDate(),
608            "  java = " + System.getProperty("java.version") }
609        )
610      );
611
612    ShutdownHookManager.get().addShutdownHook(
613      new Runnable() {
614        @Override
615        public void run() {
616          LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
617            "Shutting down " + classname + " at " + hostname}));
618        }
619      }, SHUTDOWN_HOOK_PRIORITY);
620
621  }
622
623  /**
624   * The traditional binary prefixes, kilo, mega, ..., exa,
625   * which can be represented by a 64-bit integer.
626   * TraditionalBinaryPrefix symbol are case insensitive. 
627   */
628  public static enum TraditionalBinaryPrefix {
629    KILO(1024),
630    MEGA(KILO.value << 10),
631    GIGA(MEGA.value << 10),
632    TERA(GIGA.value << 10),
633    PETA(TERA.value << 10),
634    EXA(PETA.value << 10);
635
636    public final long value;
637    public final char symbol;
638
639    TraditionalBinaryPrefix(long value) {
640      this.value = value;
641      this.symbol = toString().charAt(0);
642    }
643
644    /**
645     * @return The TraditionalBinaryPrefix object corresponding to the symbol.
646     */
647    public static TraditionalBinaryPrefix valueOf(char symbol) {
648      symbol = Character.toUpperCase(symbol);
649      for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
650        if (symbol == prefix.symbol) {
651          return prefix;
652        }
653      }
654      throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
655    }
656
657    /**
658     * Convert a string to long.
659     * The input string is first be trimmed
660     * and then it is parsed with traditional binary prefix.
661     *
662     * For example,
663     * "-1230k" will be converted to -1230 * 1024 = -1259520;
664     * "891g" will be converted to 891 * 1024^3 = 956703965184;
665     *
666     * @param s input string
667     * @return a long value represented by the input string.
668     */
669    public static long string2long(String s) {
670      s = s.trim();
671      final int lastpos = s.length() - 1;
672      final char lastchar = s.charAt(lastpos);
673      if (Character.isDigit(lastchar))
674        return Long.parseLong(s);
675      else {
676        long prefix;
677        try {
678          prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
679        } catch (IllegalArgumentException e) {
680          throw new IllegalArgumentException("Invalid size prefix '" + lastchar
681              + "' in '" + s
682              + "'. Allowed prefixes are k, m, g, t, p, e(case insensitive)");
683        }
684        long num = Long.parseLong(s.substring(0, lastpos));
685        if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
686          throw new IllegalArgumentException(s + " does not fit in a Long");
687        }
688        return num * prefix;
689      }
690    }
691  }
692  
693    /**
694     * Escapes HTML Special characters present in the string.
695     * @param string
696     * @return HTML Escaped String representation
697     */
698    public static String escapeHTML(String string) {
699      if(string == null) {
700        return null;
701      }
702      StringBuilder sb = new StringBuilder();
703      boolean lastCharacterWasSpace = false;
704      char[] chars = string.toCharArray();
705      for(char c : chars) {
706        if(c == ' ') {
707          if(lastCharacterWasSpace){
708            lastCharacterWasSpace = false;
709            sb.append("&nbsp;");
710          }else {
711            lastCharacterWasSpace=true;
712            sb.append(" ");
713          }
714        }else {
715          lastCharacterWasSpace = false;
716          switch(c) {
717          case '<': sb.append("&lt;"); break;
718          case '>': sb.append("&gt;"); break;
719          case '&': sb.append("&amp;"); break;
720          case '"': sb.append("&quot;"); break;
721          default : sb.append(c);break;
722          }
723        }
724      }
725      
726      return sb.toString();
727    }
728
729  /**
730   * Return an abbreviated English-language desc of the byte length
731   */
732  public static String byteDesc(long len) {
733    double val = 0.0;
734    String ending = "";
735    if (len < 1024 * 1024) {
736      val = (1.0 * len) / 1024;
737      ending = " KB";
738    } else if (len < 1024 * 1024 * 1024) {
739      val = (1.0 * len) / (1024 * 1024);
740      ending = " MB";
741    } else if (len < 1024L * 1024 * 1024 * 1024) {
742      val = (1.0 * len) / (1024 * 1024 * 1024);
743      ending = " GB";
744    } else if (len < 1024L * 1024 * 1024 * 1024 * 1024) {
745      val = (1.0 * len) / (1024L * 1024 * 1024 * 1024);
746      ending = " TB";
747    } else {
748      val = (1.0 * len) / (1024L * 1024 * 1024 * 1024 * 1024);
749      ending = " PB";
750    }
751    return limitDecimalTo2(val) + ending;
752  }
753
754  public static synchronized String limitDecimalTo2(double d) {
755    return decimalFormat.format(d);
756  }
757  
758  /**
759   * Concatenates strings, using a separator.
760   *
761   * @param separator Separator to join with.
762   * @param strings Strings to join.
763   */
764  public static String join(CharSequence separator, Iterable<?> strings) {
765    Iterator<?> i = strings.iterator();
766    if (!i.hasNext()) {
767      return "";
768    }
769    StringBuilder sb = new StringBuilder(i.next().toString());
770    while (i.hasNext()) {
771      sb.append(separator);
772      sb.append(i.next().toString());
773    }
774    return sb.toString();
775  }
776
777  /**
778   * Convert SOME_STUFF to SomeStuff
779   *
780   * @param s input string
781   * @return camelized string
782   */
783  public static String camelize(String s) {
784    StringBuilder sb = new StringBuilder();
785    String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
786
787    for (String word : words)
788      sb.append(org.apache.commons.lang.StringUtils.capitalize(word));
789
790    return sb.toString();
791  }
792}