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