001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package org.apache.hadoop.util;
020    
021    import java.io.PrintWriter;
022    import java.io.StringWriter;
023    import java.net.URI;
024    import java.net.URISyntaxException;
025    import java.text.DateFormat;
026    import java.util.ArrayList;
027    import java.util.Arrays;
028    import java.util.Collection;
029    import java.util.Date;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Locale;
033    import java.util.StringTokenizer;
034    
035    import org.apache.hadoop.classification.InterfaceAudience;
036    import org.apache.hadoop.classification.InterfaceStability;
037    import org.apache.hadoop.fs.Path;
038    import org.apache.hadoop.net.NetUtils;
039    
040    import com.google.common.net.InetAddresses;
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      /**
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    }