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       * @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            )
609          );
610    
611        ShutdownHookManager.get().addShutdownHook(
612          new Runnable() {
613            @Override
614            public void run() {
615              LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
616                "Shutting down " + classname + " at " + hostname}));
617            }
618          }, SHUTDOWN_HOOK_PRIORITY);
619    
620      }
621    
622      /**
623       * The traditional binary prefixes, kilo, mega, ..., exa,
624       * which can be represented by a 64-bit integer.
625       * TraditionalBinaryPrefix symbol are case insensitive. 
626       */
627      public static enum TraditionalBinaryPrefix {
628        KILO(1024),
629        MEGA(KILO.value << 10),
630        GIGA(MEGA.value << 10),
631        TERA(GIGA.value << 10),
632        PETA(TERA.value << 10),
633        EXA(PETA.value << 10);
634    
635        public final long value;
636        public final char symbol;
637    
638        TraditionalBinaryPrefix(long value) {
639          this.value = value;
640          this.symbol = toString().charAt(0);
641        }
642    
643        /**
644         * @return The TraditionalBinaryPrefix object corresponding to the symbol.
645         */
646        public static TraditionalBinaryPrefix valueOf(char symbol) {
647          symbol = Character.toUpperCase(symbol);
648          for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
649            if (symbol == prefix.symbol) {
650              return prefix;
651            }
652          }
653          throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
654        }
655    
656        /**
657         * Convert a string to long.
658         * The input string is first be trimmed
659         * and then it is parsed with traditional binary prefix.
660         *
661         * For example,
662         * "-1230k" will be converted to -1230 * 1024 = -1259520;
663         * "891g" will be converted to 891 * 1024^3 = 956703965184;
664         *
665         * @param s input string
666         * @return a long value represented by the input string.
667         */
668        public static long string2long(String s) {
669          s = s.trim();
670          final int lastpos = s.length() - 1;
671          final char lastchar = s.charAt(lastpos);
672          if (Character.isDigit(lastchar))
673            return Long.parseLong(s);
674          else {
675            long prefix;
676            try {
677              prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
678            } catch (IllegalArgumentException e) {
679              throw new IllegalArgumentException("Invalid size prefix '" + lastchar
680                  + "' in '" + s
681                  + "'. Allowed prefixes are k, m, g, t, p, e(case insensitive)");
682            }
683            long num = Long.parseLong(s.substring(0, lastpos));
684            if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
685              throw new IllegalArgumentException(s + " does not fit in a Long");
686            }
687            return num * prefix;
688          }
689        }
690      }
691      
692        /**
693         * Escapes HTML Special characters present in the string.
694         * @param string
695         * @return HTML Escaped String representation
696         */
697        public static String escapeHTML(String string) {
698          if(string == null) {
699            return null;
700          }
701          StringBuilder sb = new StringBuilder();
702          boolean lastCharacterWasSpace = false;
703          char[] chars = string.toCharArray();
704          for(char c : chars) {
705            if(c == ' ') {
706              if(lastCharacterWasSpace){
707                lastCharacterWasSpace = false;
708                sb.append("&nbsp;");
709              }else {
710                lastCharacterWasSpace=true;
711                sb.append(" ");
712              }
713            }else {
714              lastCharacterWasSpace = false;
715              switch(c) {
716              case '<': sb.append("&lt;"); break;
717              case '>': sb.append("&gt;"); break;
718              case '&': sb.append("&amp;"); break;
719              case '"': sb.append("&quot;"); break;
720              default : sb.append(c);break;
721              }
722            }
723          }
724          
725          return sb.toString();
726        }
727    
728      /**
729       * Return an abbreviated English-language desc of the byte length
730       */
731      public static String byteDesc(long len) {
732        double val = 0.0;
733        String ending = "";
734        if (len < 1024 * 1024) {
735          val = (1.0 * len) / 1024;
736          ending = " KB";
737        } else if (len < 1024 * 1024 * 1024) {
738          val = (1.0 * len) / (1024 * 1024);
739          ending = " MB";
740        } else if (len < 1024L * 1024 * 1024 * 1024) {
741          val = (1.0 * len) / (1024 * 1024 * 1024);
742          ending = " GB";
743        } else if (len < 1024L * 1024 * 1024 * 1024 * 1024) {
744          val = (1.0 * len) / (1024L * 1024 * 1024 * 1024);
745          ending = " TB";
746        } else {
747          val = (1.0 * len) / (1024L * 1024 * 1024 * 1024 * 1024);
748          ending = " PB";
749        }
750        return limitDecimalTo2(val) + ending;
751      }
752    
753      public static synchronized String limitDecimalTo2(double d) {
754        return decimalFormat.format(d);
755      }
756      
757      /**
758       * Concatenates strings, using a separator.
759       *
760       * @param separator Separator to join with.
761       * @param strings Strings to join.
762       */
763      public static String join(CharSequence separator, Iterable<?> strings) {
764        Iterator<?> i = strings.iterator();
765        if (!i.hasNext()) {
766          return "";
767        }
768        StringBuilder sb = new StringBuilder(i.next().toString());
769        while (i.hasNext()) {
770          sb.append(separator);
771          sb.append(i.next().toString());
772        }
773        return sb.toString();
774      }
775    
776      /**
777       * Convert SOME_STUFF to SomeStuff
778       *
779       * @param s input string
780       * @return camelized string
781       */
782      public static String camelize(String s) {
783        StringBuilder sb = new StringBuilder();
784        String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
785    
786        for (String word : words)
787          sb.append(org.apache.commons.lang.StringUtils.capitalize(word));
788    
789        return sb.toString();
790      }
791    }