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.text.DecimalFormat;
027    import java.text.NumberFormat;
028    import java.util.ArrayList;
029    import java.util.Arrays;
030    import java.util.Collection;
031    import java.util.Date;
032    import java.util.Iterator;
033    import java.util.List;
034    import java.util.Locale;
035    import java.util.StringTokenizer;
036    
037    import org.apache.hadoop.classification.InterfaceAudience;
038    import org.apache.hadoop.classification.InterfaceStability;
039    import org.apache.hadoop.fs.Path;
040    import org.apache.hadoop.net.NetUtils;
041    
042    /**
043     * General string utils
044     */
045    @InterfaceAudience.Private
046    @InterfaceStability.Unstable
047    public 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       * 
206       * @param str
207       */
208      public static URI[] stringToURI(String[] str){
209        if (str == null) 
210          return null;
211        URI[] uris = new URI[str.length];
212        for (int i = 0; i < str.length;i++){
213          try{
214            uris[i] = new URI(str[i]);
215          }catch(URISyntaxException ur){
216            System.out.println("Exception in specified URI's " + StringUtils.stringifyException(ur));
217            //making sure its asssigned to null in case of an error
218            uris[i] = null;
219          }
220        }
221        return uris;
222      }
223      
224      /**
225       * 
226       * @param str
227       */
228      public static Path[] stringToPath(String[] str){
229        if (str == null) {
230          return null;
231        }
232        Path[] p = new Path[str.length];
233        for (int i = 0; i < str.length;i++){
234          p[i] = new Path(str[i]);
235        }
236        return p;
237      }
238      /**
239       * 
240       * Given a finish and start time in long milliseconds, returns a 
241       * String in the format Xhrs, Ymins, Z sec, for the time difference between two times. 
242       * If finish time comes before start time then negative valeus of X, Y and Z wil return. 
243       * 
244       * @param finishTime finish time
245       * @param startTime start time
246       */
247      public static String formatTimeDiff(long finishTime, long startTime){
248        long timeDiff = finishTime - startTime; 
249        return formatTime(timeDiff); 
250      }
251      
252      /**
253       * 
254       * Given the time in long milliseconds, returns a 
255       * String in the format Xhrs, Ymins, Z sec. 
256       * 
257       * @param timeDiff The time difference to format
258       */
259      public static String formatTime(long timeDiff){
260        StringBuilder buf = new StringBuilder();
261        long hours = timeDiff / (60*60*1000);
262        long rem = (timeDiff % (60*60*1000));
263        long minutes =  rem / (60*1000);
264        rem = rem % (60*1000);
265        long seconds = rem / 1000;
266        
267        if (hours != 0){
268          buf.append(hours);
269          buf.append("hrs, ");
270        }
271        if (minutes != 0){
272          buf.append(minutes);
273          buf.append("mins, ");
274        }
275        // return "0sec if no difference
276        buf.append(seconds);
277        buf.append("sec");
278        return buf.toString(); 
279      }
280      /**
281       * Formats time in ms and appends difference (finishTime - startTime) 
282       * as returned by formatTimeDiff().
283       * If finish time is 0, empty string is returned, if start time is 0 
284       * then difference is not appended to return value. 
285       * @param dateFormat date format to use
286       * @param finishTime fnish time
287       * @param startTime start time
288       * @return formatted value. 
289       */
290      public static String getFormattedTimeWithDiff(DateFormat dateFormat, 
291                                                    long finishTime, long startTime){
292        StringBuilder buf = new StringBuilder();
293        if (0 != finishTime) {
294          buf.append(dateFormat.format(new Date(finishTime)));
295          if (0 != startTime){
296            buf.append(" (" + formatTimeDiff(finishTime , startTime) + ")");
297          }
298        }
299        return buf.toString();
300      }
301      
302      /**
303       * Returns an arraylist of strings.
304       * @param str the comma seperated string values
305       * @return the arraylist of the comma seperated string values
306       */
307      public static String[] getStrings(String str){
308        Collection<String> values = getStringCollection(str);
309        if(values.size() == 0) {
310          return null;
311        }
312        return values.toArray(new String[values.size()]);
313      }
314    
315      /**
316       * Returns a collection of strings.
317       * @param str comma seperated string values
318       * @return an <code>ArrayList</code> of string values
319       */
320      public static Collection<String> getStringCollection(String str){
321        List<String> values = new ArrayList<String>();
322        if (str == null)
323          return values;
324        StringTokenizer tokenizer = new StringTokenizer (str,",");
325        values = new ArrayList<String>();
326        while (tokenizer.hasMoreTokens()) {
327          values.add(tokenizer.nextToken());
328        }
329        return values;
330      }
331    
332      /**
333       * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
334       * @param str a comma separated <String> with values
335       * @return a <code>Collection</code> of <code>String</code> values
336       */
337      public static Collection<String> getTrimmedStringCollection(String str){
338        return new ArrayList<String>(
339          Arrays.asList(getTrimmedStrings(str)));
340      }
341      
342      /**
343       * Splits a comma separated value <code>String</code>, trimming leading and trailing whitespace on each value.
344       * @param str a comma separated <String> with values
345       * @return an array of <code>String</code> values
346       */
347      public static String[] getTrimmedStrings(String str){
348        if (null == str || "".equals(str.trim())) {
349          return emptyStringArray;
350        }
351    
352        return str.trim().split("\\s*,\\s*");
353      }
354    
355      final public static String[] emptyStringArray = {};
356      final public static char COMMA = ',';
357      final public static String COMMA_STR = ",";
358      final public static char ESCAPE_CHAR = '\\';
359      
360      /**
361       * Split a string using the default separator
362       * @param str a string that may have escaped separator
363       * @return an array of strings
364       */
365      public static String[] split(String str) {
366        return split(str, ESCAPE_CHAR, COMMA);
367      }
368      
369      /**
370       * Split a string using the given separator
371       * @param str a string that may have escaped separator
372       * @param escapeChar a char that be used to escape the separator
373       * @param separator a separator char
374       * @return an array of strings
375       */
376      public static String[] split(
377          String str, char escapeChar, char separator) {
378        if (str==null) {
379          return null;
380        }
381        ArrayList<String> strList = new ArrayList<String>();
382        StringBuilder split = new StringBuilder();
383        int index = 0;
384        while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
385          ++index; // move over the separator for next search
386          strList.add(split.toString());
387          split.setLength(0); // reset the buffer 
388        }
389        strList.add(split.toString());
390        // remove trailing empty split(s)
391        int last = strList.size(); // last split
392        while (--last>=0 && "".equals(strList.get(last))) {
393          strList.remove(last);
394        }
395        return strList.toArray(new String[strList.size()]);
396      }
397    
398      /**
399       * Split a string using the given separator, with no escaping performed.
400       * @param str a string to be split. Note that this may not be null.
401       * @param separator a separator char
402       * @return an array of strings
403       */
404      public static String[] split(
405          String str, char separator) {
406        // String.split returns a single empty result for splitting the empty
407        // string.
408        if ("".equals(str)) {
409          return new String[]{""};
410        }
411        ArrayList<String> strList = new ArrayList<String>();
412        int startIndex = 0;
413        int nextIndex = 0;
414        while ((nextIndex = str.indexOf((int)separator, startIndex)) != -1) {
415          strList.add(str.substring(startIndex, nextIndex));
416          startIndex = nextIndex + 1;
417        }
418        strList.add(str.substring(startIndex));
419        // remove trailing empty split(s)
420        int last = strList.size(); // last split
421        while (--last>=0 && "".equals(strList.get(last))) {
422          strList.remove(last);
423        }
424        return strList.toArray(new String[strList.size()]);
425      }
426      
427      /**
428       * Finds the first occurrence of the separator character ignoring the escaped
429       * separators starting from the index. Note the substring between the index
430       * and the position of the separator is passed.
431       * @param str the source string
432       * @param separator the character to find
433       * @param escapeChar character used to escape
434       * @param start from where to search
435       * @param split used to pass back the extracted string
436       */
437      public static int findNext(String str, char separator, char escapeChar, 
438                                 int start, StringBuilder split) {
439        int numPreEscapes = 0;
440        for (int i = start; i < str.length(); i++) {
441          char curChar = str.charAt(i);
442          if (numPreEscapes == 0 && curChar == separator) { // separator 
443            return i;
444          } else {
445            split.append(curChar);
446            numPreEscapes = (curChar == escapeChar)
447                            ? (++numPreEscapes) % 2
448                            : 0;
449          }
450        }
451        return -1;
452      }
453      
454      /**
455       * Escape commas in the string using the default escape char
456       * @param str a string
457       * @return an escaped string
458       */
459      public static String escapeString(String str) {
460        return escapeString(str, ESCAPE_CHAR, COMMA);
461      }
462      
463      /**
464       * Escape <code>charToEscape</code> in the string 
465       * with the escape char <code>escapeChar</code>
466       * 
467       * @param str string
468       * @param escapeChar escape char
469       * @param charToEscape the char to be escaped
470       * @return an escaped string
471       */
472      public static String escapeString(
473          String str, char escapeChar, char charToEscape) {
474        return escapeString(str, escapeChar, new char[] {charToEscape});
475      }
476      
477      // check if the character array has the character 
478      private static boolean hasChar(char[] chars, char character) {
479        for (char target : chars) {
480          if (character == target) {
481            return true;
482          }
483        }
484        return false;
485      }
486      
487      /**
488       * @param charsToEscape array of characters to be escaped
489       */
490      public static String escapeString(String str, char escapeChar, 
491                                        char[] charsToEscape) {
492        if (str == null) {
493          return null;
494        }
495        StringBuilder result = new StringBuilder();
496        for (int i=0; i<str.length(); i++) {
497          char curChar = str.charAt(i);
498          if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
499            // special char
500            result.append(escapeChar);
501          }
502          result.append(curChar);
503        }
504        return result.toString();
505      }
506      
507      /**
508       * Unescape commas in the string using the default escape char
509       * @param str a string
510       * @return an unescaped string
511       */
512      public static String unEscapeString(String str) {
513        return unEscapeString(str, ESCAPE_CHAR, COMMA);
514      }
515      
516      /**
517       * Unescape <code>charToEscape</code> in the string 
518       * with the escape char <code>escapeChar</code>
519       * 
520       * @param str string
521       * @param escapeChar escape char
522       * @param charToEscape the escaped char
523       * @return an unescaped string
524       */
525      public static String unEscapeString(
526          String str, char escapeChar, char charToEscape) {
527        return unEscapeString(str, escapeChar, new char[] {charToEscape});
528      }
529      
530      /**
531       * @param charsToEscape array of characters to unescape
532       */
533      public static String unEscapeString(String str, char escapeChar, 
534                                          char[] charsToEscape) {
535        if (str == null) {
536          return null;
537        }
538        StringBuilder result = new StringBuilder(str.length());
539        boolean hasPreEscape = false;
540        for (int i=0; i<str.length(); i++) {
541          char curChar = str.charAt(i);
542          if (hasPreEscape) {
543            if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
544              // no special char
545              throw new IllegalArgumentException("Illegal escaped string " + str + 
546                  " unescaped " + escapeChar + " at " + (i-1));
547            } 
548            // otherwise discard the escape char
549            result.append(curChar);
550            hasPreEscape = false;
551          } else {
552            if (hasChar(charsToEscape, curChar)) {
553              throw new IllegalArgumentException("Illegal escaped string " + str + 
554                  " unescaped " + curChar + " at " + i);
555            } else if (curChar == escapeChar) {
556              hasPreEscape = true;
557            } else {
558              result.append(curChar);
559            }
560          }
561        }
562        if (hasPreEscape ) {
563          throw new IllegalArgumentException("Illegal escaped string " + str + 
564              ", not expecting " + escapeChar + " in the end." );
565        }
566        return result.toString();
567      }
568      
569      /**
570       * Return a message for logging.
571       * @param prefix prefix keyword for the message
572       * @param msg content of the message
573       * @return a message for logging
574       */
575      private static String toStartupShutdownString(String prefix, String [] msg) {
576        StringBuilder b = new StringBuilder(prefix);
577        b.append("\n/************************************************************");
578        for(String s : msg)
579          b.append("\n" + prefix + s);
580        b.append("\n************************************************************/");
581        return b.toString();
582      }
583    
584      /**
585       * Print a log message for starting up and shutting down
586       * @param clazz the class of the server
587       * @param args arguments
588       * @param LOG the target log object
589       */
590      public static void startupShutdownMessage(Class<?> clazz, String[] args,
591                                         final org.apache.commons.logging.Log LOG) {
592        final String hostname = NetUtils.getHostname();
593        final String classname = clazz.getSimpleName();
594        LOG.info(
595            toStartupShutdownString("STARTUP_MSG: ", new String[] {
596                "Starting " + classname,
597                "  host = " + hostname,
598                "  args = " + Arrays.asList(args),
599                "  version = " + VersionInfo.getVersion(),
600                "  classpath = " + System.getProperty("java.class.path"),
601                "  build = " + VersionInfo.getUrl() + " -r "
602                             + VersionInfo.getRevision()  
603                             + "; compiled by '" + VersionInfo.getUser()
604                             + "' on " + VersionInfo.getDate()}
605            )
606          );
607    
608        ShutdownHookManager.get().addShutdownHook(
609          new Runnable() {
610            @Override
611            public void run() {
612              LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
613                "Shutting down " + classname + " at " + hostname}));
614            }
615          }, SHUTDOWN_HOOK_PRIORITY);
616    
617      }
618    
619      /**
620       * The traditional binary prefixes, kilo, mega, ..., exa,
621       * which can be represented by a 64-bit integer.
622       * TraditionalBinaryPrefix symbol are case insensitive. 
623       */
624      public static enum TraditionalBinaryPrefix {
625        KILO(1024),
626        MEGA(KILO.value << 10),
627        GIGA(MEGA.value << 10),
628        TERA(GIGA.value << 10),
629        PETA(TERA.value << 10),
630        EXA(PETA.value << 10);
631    
632        public final long value;
633        public final char symbol;
634    
635        TraditionalBinaryPrefix(long value) {
636          this.value = value;
637          this.symbol = toString().charAt(0);
638        }
639    
640        /**
641         * @return The TraditionalBinaryPrefix object corresponding to the symbol.
642         */
643        public static TraditionalBinaryPrefix valueOf(char symbol) {
644          symbol = Character.toUpperCase(symbol);
645          for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
646            if (symbol == prefix.symbol) {
647              return prefix;
648            }
649          }
650          throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
651        }
652    
653        /**
654         * Convert a string to long.
655         * The input string is first be trimmed
656         * and then it is parsed with traditional binary prefix.
657         *
658         * For example,
659         * "-1230k" will be converted to -1230 * 1024 = -1259520;
660         * "891g" will be converted to 891 * 1024^3 = 956703965184;
661         *
662         * @param s input string
663         * @return a long value represented by the input string.
664         */
665        public static long string2long(String s) {
666          s = s.trim();
667          final int lastpos = s.length() - 1;
668          final char lastchar = s.charAt(lastpos);
669          if (Character.isDigit(lastchar))
670            return Long.parseLong(s);
671          else {
672            long prefix;
673            try {
674              prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
675            } catch (IllegalArgumentException e) {
676              throw new IllegalArgumentException("Invalid size prefix '" + lastchar
677                  + "' in '" + s
678                  + "'. Allowed prefixes are k, m, g, t, p, e(case insensitive)");
679            }
680            long num = Long.parseLong(s.substring(0, lastpos));
681            if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
682              throw new IllegalArgumentException(s + " does not fit in a Long");
683            }
684            return num * prefix;
685          }
686        }
687      }
688      
689        /**
690         * Escapes HTML Special characters present in the string.
691         * @param string
692         * @return HTML Escaped String representation
693         */
694        public static String escapeHTML(String string) {
695          if(string == null) {
696            return null;
697          }
698          StringBuilder sb = new StringBuilder();
699          boolean lastCharacterWasSpace = false;
700          char[] chars = string.toCharArray();
701          for(char c : chars) {
702            if(c == ' ') {
703              if(lastCharacterWasSpace){
704                lastCharacterWasSpace = false;
705                sb.append("&nbsp;");
706              }else {
707                lastCharacterWasSpace=true;
708                sb.append(" ");
709              }
710            }else {
711              lastCharacterWasSpace = false;
712              switch(c) {
713              case '<': sb.append("&lt;"); break;
714              case '>': sb.append("&gt;"); break;
715              case '&': sb.append("&amp;"); break;
716              case '"': sb.append("&quot;"); break;
717              default : sb.append(c);break;
718              }
719            }
720          }
721          
722          return sb.toString();
723        }
724    
725      /**
726       * Return an abbreviated English-language desc of the byte length
727       */
728      public static String byteDesc(long len) {
729        double val = 0.0;
730        String ending = "";
731        if (len < 1024 * 1024) {
732          val = (1.0 * len) / 1024;
733          ending = " KB";
734        } else if (len < 1024 * 1024 * 1024) {
735          val = (1.0 * len) / (1024 * 1024);
736          ending = " MB";
737        } else if (len < 1024L * 1024 * 1024 * 1024) {
738          val = (1.0 * len) / (1024 * 1024 * 1024);
739          ending = " GB";
740        } else if (len < 1024L * 1024 * 1024 * 1024 * 1024) {
741          val = (1.0 * len) / (1024L * 1024 * 1024 * 1024);
742          ending = " TB";
743        } else {
744          val = (1.0 * len) / (1024L * 1024 * 1024 * 1024 * 1024);
745          ending = " PB";
746        }
747        return limitDecimalTo2(val) + ending;
748      }
749    
750      public static synchronized String limitDecimalTo2(double d) {
751        return decimalFormat.format(d);
752      }
753      
754      /**
755       * Concatenates strings, using a separator.
756       *
757       * @param separator Separator to join with.
758       * @param strings Strings to join.
759       */
760      public static String join(CharSequence separator, Iterable<?> strings) {
761        Iterator<?> i = strings.iterator();
762        if (!i.hasNext()) {
763          return "";
764        }
765        StringBuilder sb = new StringBuilder(i.next().toString());
766        while (i.hasNext()) {
767          sb.append(separator);
768          sb.append(i.next().toString());
769        }
770        return sb.toString();
771      }
772    
773      /**
774       * Convert SOME_STUFF to SomeStuff
775       *
776       * @param s input string
777       * @return camelized string
778       */
779      public static String camelize(String s) {
780        StringBuilder sb = new StringBuilder();
781        String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
782    
783        for (String word : words)
784          sb.append(org.apache.commons.lang.StringUtils.capitalize(word));
785    
786        return sb.toString();
787      }
788    }