1 /**
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 
19 package org.apache.hadoop.util;
20 
21 import java.io.PrintWriter;
22 import java.io.StringWriter;
23 import java.net.InetAddress;
24 import java.net.URI;
25 import java.net.URISyntaxException;
26 import java.net.UnknownHostException;
27 import java.text.DateFormat;
28 import java.text.DecimalFormat;
29 import java.text.NumberFormat;
30 import java.util.Locale;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Date;
34 import java.util.List;
35 import java.util.StringTokenizer;
36 import java.util.Collection;
37 
38 import org.apache.hadoop.fs.*;
39 
40 /**
41  * General string utils
42  */
43 public class StringUtils {
44 
45   private static final DecimalFormat decimalFormat;
46   static {
47           NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.ENGLISH);
48           decimalFormat = (DecimalFormat) numberFormat;
49           decimalFormat.applyPattern("#.##");
50   }
51 
52   /**
53    * Make a string representation of the exception.
54    * @param e The exception to stringify
55    * @return A string with exception name and call stack.
56    */
stringifyException(Throwable e)57   public static String stringifyException(Throwable e) {
58     StringWriter stm = new StringWriter();
59     PrintWriter wrt = new PrintWriter(stm);
60     e.printStackTrace(wrt);
61     wrt.close();
62     return stm.toString();
63   }
64 
65   /**
66    * Given a full hostname, return the word upto the first dot.
67    * @param fullHostname the full hostname
68    * @return the hostname to the first dot
69    */
simpleHostname(String fullHostname)70   public static String simpleHostname(String fullHostname) {
71     int offset = fullHostname.indexOf('.');
72     if (offset != -1) {
73       return fullHostname.substring(0, offset);
74     }
75     return fullHostname;
76   }
77 
78   private static DecimalFormat oneDecimal = new DecimalFormat("0.0");
79 
80   /**
81    * Given an integer, return a string that is in an approximate, but human
82    * readable format.
83    * It uses the bases 'k', 'm', and 'g' for 1024, 1024**2, and 1024**3.
84    * @param number the number to format
85    * @return a human readable form of the integer
86    */
humanReadableInt(long number)87   public static String humanReadableInt(long number) {
88     long absNumber = Math.abs(number);
89     double result = number;
90     String suffix = "";
91     if (absNumber < 1024) {
92       // nothing
93     } else if (absNumber < 1024 * 1024) {
94       result = number / 1024.0;
95       suffix = "k";
96     } else if (absNumber < 1024 * 1024 * 1024) {
97       result = number / (1024.0 * 1024);
98       suffix = "m";
99     } else {
100       result = number / (1024.0 * 1024 * 1024);
101       suffix = "g";
102     }
103     return oneDecimal.format(result) + suffix;
104   }
105 
106   /**
107    * Format a percentage for presentation to the user.
108    * @param done the percentage to format (0.0 to 1.0)
109    * @param digits the number of digits past the decimal point
110    * @return a string representation of the percentage
111    */
formatPercent(double done, int digits)112   public static String formatPercent(double done, int digits) {
113     DecimalFormat percentFormat = new DecimalFormat("0.00%");
114     double scale = Math.pow(10.0, digits+2);
115     double rounded = Math.floor(done * scale);
116     percentFormat.setDecimalSeparatorAlwaysShown(false);
117     percentFormat.setMinimumFractionDigits(digits);
118     percentFormat.setMaximumFractionDigits(digits);
119     return percentFormat.format(rounded / scale);
120   }
121 
122   /**
123    * Given an array of strings, return a comma-separated list of its elements.
124    * @param strs Array of strings
125    * @return Empty string if strs.length is 0, comma separated list of strings
126    * otherwise
127    */
128 
arrayToString(String[] strs)129   public static String arrayToString(String[] strs) {
130     if (strs.length == 0) { return ""; }
131     StringBuffer sbuf = new StringBuffer();
132     sbuf.append(strs[0]);
133     for (int idx = 1; idx < strs.length; idx++) {
134       sbuf.append(",");
135       sbuf.append(strs[idx]);
136     }
137     return sbuf.toString();
138   }
139 
140   /**
141    * Given an array of bytes it will convert the bytes to a hex string
142    * representation of the bytes
143    * @param bytes
144    * @param start start index, inclusively
145    * @param end end index, exclusively
146    * @return hex string representation of the byte array
147    */
byteToHexString(byte[] bytes, int start, int end)148   public static String byteToHexString(byte[] bytes, int start, int end) {
149     if (bytes == null) {
150       throw new IllegalArgumentException("bytes == null");
151     }
152     StringBuilder s = new StringBuilder();
153     for(int i = start; i < end; i++) {
154       s.append(String.format("%02x", bytes[i]));
155     }
156     return s.toString();
157   }
158 
159   /** Same as byteToHexString(bytes, 0, bytes.length). */
byteToHexString(byte bytes[])160   public static String byteToHexString(byte bytes[]) {
161     return byteToHexString(bytes, 0, bytes.length);
162   }
163 
164   /**
165    * Given a hexstring this will return the byte array corresponding to the
166    * string
167    * @param hex the hex String array
168    * @return a byte array that is a hex string representation of the given
169    *         string. The size of the byte array is therefore hex.length/2
170    */
hexStringToByte(String hex)171   public static byte[] hexStringToByte(String hex) {
172     byte[] bts = new byte[hex.length() / 2];
173     for (int i = 0; i < bts.length; i++) {
174       bts[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
175     }
176     return bts;
177   }
178   /**
179    *
180    * @param uris
181    */
uriToString(URI[] uris)182   public static String uriToString(URI[] uris){
183     if (uris == null) {
184       return null;
185     }
186     StringBuffer ret = new StringBuffer(uris[0].toString());
187     for(int i = 1; i < uris.length;i++){
188       ret.append(",");
189       ret.append(uris[i].toString());
190     }
191     return ret.toString();
192   }
193 
194   /**
195    *
196    * @param str
197    */
stringToURI(String[] str)198   public static URI[] stringToURI(String[] str){
199     if (str == null)
200       return null;
201     URI[] uris = new URI[str.length];
202     for (int i = 0; i < str.length;i++){
203       try{
204         uris[i] = new URI(str[i]);
205       }catch(URISyntaxException ur){
206         System.out.println("Exception in specified URI's " + StringUtils.stringifyException(ur));
207         //making sure its asssigned to null in case of an error
208         uris[i] = null;
209       }
210     }
211     return uris;
212   }
213 
214   /**
215    *
216    * @param str
217    */
stringToPath(String[] str)218   public static Path[] stringToPath(String[] str){
219     if (str == null) {
220       return null;
221     }
222     Path[] p = new Path[str.length];
223     for (int i = 0; i < str.length;i++){
224       p[i] = new Path(str[i]);
225     }
226     return p;
227   }
228   /**
229    *
230    * Given a finish and start time in long milliseconds, returns a
231    * String in the format Xhrs, Ymins, Z sec, for the time difference between two times.
232    * If finish time comes before start time then negative valeus of X, Y and Z wil return.
233    *
234    * @param finishTime finish time
235    * @param startTime start time
236    */
formatTimeDiff(long finishTime, long startTime)237   public static String formatTimeDiff(long finishTime, long startTime){
238     long timeDiff = finishTime - startTime;
239     return formatTime(timeDiff);
240   }
241 
242   /**
243    *
244    * Given the time in long milliseconds, returns a
245    * String in the format Xhrs, Ymins, Z sec.
246    *
247    * @param timeDiff The time difference to format
248    */
formatTime(long timeDiff)249   public static String formatTime(long timeDiff){
250     StringBuffer buf = new StringBuffer();
251     long hours = timeDiff / (60*60*1000);
252     long rem = (timeDiff % (60*60*1000));
253     long minutes =  rem / (60*1000);
254     rem = rem % (60*1000);
255     long seconds = rem / 1000;
256 
257     if (hours != 0){
258       buf.append(hours);
259       buf.append("hrs, ");
260     }
261     if (minutes != 0){
262       buf.append(minutes);
263       buf.append("mins, ");
264     }
265     // return "0sec if no difference
266     buf.append(seconds);
267     buf.append("sec");
268     return buf.toString();
269   }
270   /**
271    * Formats time in ms and appends difference (finishTime - startTime)
272    * as returned by formatTimeDiff().
273    * If finish time is 0, empty string is returned, if start time is 0
274    * then difference is not appended to return value.
275    * @param dateFormat date format to use
276    * @param finishTime fnish time
277    * @param startTime start time
278    * @return formatted value.
279    */
getFormattedTimeWithDiff(DateFormat dateFormat, long finishTime, long startTime)280   public static String getFormattedTimeWithDiff(DateFormat dateFormat,
281                                                 long finishTime, long startTime){
282     StringBuffer buf = new StringBuffer();
283     if (0 != finishTime) {
284       buf.append(dateFormat.format(new Date(finishTime)));
285       if (0 != startTime){
286         buf.append(" (" + formatTimeDiff(finishTime , startTime) + ")");
287       }
288     }
289     return buf.toString();
290   }
291 
292   /**
293    * Returns an arraylist of strings.
294    * @param str the comma seperated string values
295    * @return the arraylist of the comma seperated string values
296    */
getStrings(String str)297   public static String[] getStrings(String str){
298     Collection<String> values = getStringCollection(str);
299     if(values.size() == 0) {
300       return null;
301     }
302     return values.toArray(new String[values.size()]);
303   }
304 
305   /**
306    * Returns a collection of strings.
307    * @param str comma seperated string values
308    * @return an <code>ArrayList</code> of string values
309    */
getStringCollection(String str)310   public static Collection<String> getStringCollection(String str){
311     List<String> values = new ArrayList<String>();
312     if (str == null)
313       return values;
314     StringTokenizer tokenizer = new StringTokenizer (str,",");
315     values = new ArrayList<String>();
316     while (tokenizer.hasMoreTokens()) {
317       values.add(tokenizer.nextToken());
318     }
319     return values;
320   }
321 
322   final public static char COMMA = ',';
323   final public static String COMMA_STR = ",";
324   final public static char ESCAPE_CHAR = '\\';
325 
326   /**
327    * Split a string using the default separator
328    * @param str a string that may have escaped separator
329    * @return an array of strings
330    */
split(String str)331   public static String[] split(String str) {
332     return split(str, ESCAPE_CHAR, COMMA);
333   }
334 
335   /**
336    * Split a string using the given separator
337    * @param str a string that may have escaped separator
338    * @param escapeChar a char that be used to escape the separator
339    * @param separator a separator char
340    * @return an array of strings
341    */
split( String str, char escapeChar, char separator)342   public static String[] split(
343       String str, char escapeChar, char separator) {
344     if (str==null) {
345       return null;
346     }
347     ArrayList<String> strList = new ArrayList<String>();
348     StringBuilder split = new StringBuilder();
349     int index = 0;
350     while ((index = findNext(str, separator, escapeChar, index, split)) >= 0) {
351       ++index; // move over the separator for next search
352       strList.add(split.toString());
353       split.setLength(0); // reset the buffer
354     }
355     strList.add(split.toString());
356     // remove trailing empty split(s)
357     int last = strList.size(); // last split
358     while (--last>=0 && "".equals(strList.get(last))) {
359       strList.remove(last);
360     }
361     return strList.toArray(new String[strList.size()]);
362   }
363 
364   /**
365    * Finds the first occurrence of the separator character ignoring the escaped
366    * separators starting from the index. Note the substring between the index
367    * and the position of the separator is passed.
368    * @param str the source string
369    * @param separator the character to find
370    * @param escapeChar character used to escape
371    * @param start from where to search
372    * @param split used to pass back the extracted string
373    */
findNext(String str, char separator, char escapeChar, int start, StringBuilder split)374   public static int findNext(String str, char separator, char escapeChar,
375                              int start, StringBuilder split) {
376     int numPreEscapes = 0;
377     for (int i = start; i < str.length(); i++) {
378       char curChar = str.charAt(i);
379       if (numPreEscapes == 0 && curChar == separator) { // separator
380         return i;
381       } else {
382         split.append(curChar);
383         numPreEscapes = (curChar == escapeChar)
384                         ? (++numPreEscapes) % 2
385                         : 0;
386       }
387     }
388     return -1;
389   }
390 
391   /**
392    * Escape commas in the string using the default escape char
393    * @param str a string
394    * @return an escaped string
395    */
escapeString(String str)396   public static String escapeString(String str) {
397     return escapeString(str, ESCAPE_CHAR, COMMA);
398   }
399 
400   /**
401    * Escape <code>charToEscape</code> in the string
402    * with the escape char <code>escapeChar</code>
403    *
404    * @param str string
405    * @param escapeChar escape char
406    * @param charToEscape the char to be escaped
407    * @return an escaped string
408    */
escapeString( String str, char escapeChar, char charToEscape)409   public static String escapeString(
410       String str, char escapeChar, char charToEscape) {
411     return escapeString(str, escapeChar, new char[] {charToEscape});
412   }
413 
414   // check if the character array has the character
hasChar(char[] chars, char character)415   private static boolean hasChar(char[] chars, char character) {
416     for (char target : chars) {
417       if (character == target) {
418         return true;
419       }
420     }
421     return false;
422   }
423 
424   /**
425    * @param charsToEscape array of characters to be escaped
426    */
escapeString(String str, char escapeChar, char[] charsToEscape)427   public static String escapeString(String str, char escapeChar,
428                                     char[] charsToEscape) {
429     if (str == null) {
430       return null;
431     }
432     int len = str.length();
433     // Let us specify good enough capacity to constructor of StringBuilder sothat
434     // resizing would not be needed(to improve perf).
435     StringBuilder result = new StringBuilder((int)(len * 1.5));
436 
437     for (int i=0; i<len; i++) {
438       char curChar = str.charAt(i);
439       if (curChar == escapeChar || hasChar(charsToEscape, curChar)) {
440         // special char
441         result.append(escapeChar);
442       }
443       result.append(curChar);
444     }
445     return result.toString();
446   }
447 
448   /**
449    * Unescape commas in the string using the default escape char
450    * @param str a string
451    * @return an unescaped string
452    */
unEscapeString(String str)453   public static String unEscapeString(String str) {
454     return unEscapeString(str, ESCAPE_CHAR, COMMA);
455   }
456 
457   /**
458    * Unescape <code>charToEscape</code> in the string
459    * with the escape char <code>escapeChar</code>
460    *
461    * @param str string
462    * @param escapeChar escape char
463    * @param charToEscape the escaped char
464    * @return an unescaped string
465    */
unEscapeString( String str, char escapeChar, char charToEscape)466   public static String unEscapeString(
467       String str, char escapeChar, char charToEscape) {
468     return unEscapeString(str, escapeChar, new char[] {charToEscape});
469   }
470 
471   /**
472    * @param charsToEscape array of characters to unescape
473    */
unEscapeString(String str, char escapeChar, char[] charsToEscape)474   public static String unEscapeString(String str, char escapeChar,
475                                       char[] charsToEscape) {
476     if (str == null) {
477       return null;
478     }
479     StringBuilder result = new StringBuilder(str.length());
480     boolean hasPreEscape = false;
481     for (int i=0; i<str.length(); i++) {
482       char curChar = str.charAt(i);
483       if (hasPreEscape) {
484         if (curChar != escapeChar && !hasChar(charsToEscape, curChar)) {
485           // no special char
486           throw new IllegalArgumentException("Illegal escaped string " + str +
487               " unescaped " + escapeChar + " at " + (i-1));
488         }
489         // otherwise discard the escape char
490         result.append(curChar);
491         hasPreEscape = false;
492       } else {
493         if (hasChar(charsToEscape, curChar)) {
494           throw new IllegalArgumentException("Illegal escaped string " + str +
495               " unescaped " + curChar + " at " + i);
496         } else if (curChar == escapeChar) {
497           hasPreEscape = true;
498         } else {
499           result.append(curChar);
500         }
501       }
502     }
503     if (hasPreEscape ) {
504       throw new IllegalArgumentException("Illegal escaped string " + str +
505           ", not expecting " + escapeChar + " in the end." );
506     }
507     return result.toString();
508   }
509 
510   /**
511    * Return hostname without throwing exception.
512    * @return hostname
513    */
getHostname()514   public static String getHostname() {
515     try {return "" + InetAddress.getLocalHost();}
516     catch(UnknownHostException uhe) {return "" + uhe;}
517   }
518 
519   /**
520    * Return a message for logging.
521    * @param prefix prefix keyword for the message
522    * @param msg content of the message
523    * @return a message for logging
524    */
toStartupShutdownString(String prefix, String [] msg)525   private static String toStartupShutdownString(String prefix, String [] msg) {
526     StringBuffer b = new StringBuffer(prefix);
527     b.append("\n/************************************************************");
528     for(String s : msg)
529       b.append("\n" + prefix + s);
530     b.append("\n************************************************************/");
531     return b.toString();
532   }
533 
534   /**
535    * Print a log message for starting up and shutting down
536    * @param clazz the class of the server
537    * @param args arguments
538    * @param LOG the target log object
539    */
startupShutdownMessage(Class<?> clazz, String[] args, final org.apache.commons.logging.Log LOG)540   public static void startupShutdownMessage(Class<?> clazz, String[] args,
541                                      final org.apache.commons.logging.Log LOG) {
542     final String hostname = getHostname();
543     final String classname = clazz.getSimpleName();
544     LOG.info(
545         toStartupShutdownString("STARTUP_MSG: ", new String[] {
546             "Starting " + classname,
547             "  host = " + hostname,
548             "  args = " + Arrays.asList(args),
549             "  version = " + VersionInfo.getVersion(),
550             "  build = " + VersionInfo.getUrl() + " -r "
551                          + VersionInfo.getRevision()
552                          + "; compiled by '" + VersionInfo.getUser()
553                          + "' on " + VersionInfo.getDate(),
554             "  java = " + System.getProperty("java.version") }
555         )
556       );
557 
558     Runtime.getRuntime().addShutdownHook(new Thread() {
559       public void run() {
560         LOG.info(toStartupShutdownString("SHUTDOWN_MSG: ", new String[]{
561           "Shutting down " + classname + " at " + hostname}));
562       }
563     });
564   }
565 
566   /**
567    * The traditional binary prefixes, kilo, mega, ..., exa,
568    * which can be represented by a 64-bit integer.
569    * TraditionalBinaryPrefix symbol are case insensitive.
570    */
571   public static enum TraditionalBinaryPrefix {
572     KILO(1024),
573     MEGA(KILO.value << 10),
574     GIGA(MEGA.value << 10),
575     TERA(GIGA.value << 10),
576     PETA(TERA.value << 10),
577     EXA(PETA.value << 10);
578 
579     public final long value;
580     public final char symbol;
581 
TraditionalBinaryPrefix(long value)582     TraditionalBinaryPrefix(long value) {
583       this.value = value;
584       this.symbol = toString().charAt(0);
585     }
586 
587     /**
588      * @return The TraditionalBinaryPrefix object corresponding to the symbol.
589      */
valueOf(char symbol)590     public static TraditionalBinaryPrefix valueOf(char symbol) {
591       symbol = Character.toUpperCase(symbol);
592       for(TraditionalBinaryPrefix prefix : TraditionalBinaryPrefix.values()) {
593         if (symbol == prefix.symbol) {
594           return prefix;
595         }
596       }
597       throw new IllegalArgumentException("Unknown symbol '" + symbol + "'");
598     }
599 
600     /**
601      * Convert a string to long.
602      * The input string is first be trimmed
603      * and then it is parsed with traditional binary prefix.
604      *
605      * For example,
606      * "-1230k" will be converted to -1230 * 1024 = -1259520;
607      * "891g" will be converted to 891 * 1024^3 = 956703965184;
608      *
609      * @param s input string
610      * @return a long value represented by the input string.
611      */
string2long(String s)612     public static long string2long(String s) {
613       s = s.trim();
614       final int lastpos = s.length() - 1;
615       final char lastchar = s.charAt(lastpos);
616       if (Character.isDigit(lastchar))
617         return Long.parseLong(s);
618       else {
619         long prefix = TraditionalBinaryPrefix.valueOf(lastchar).value;
620         long num = Long.parseLong(s.substring(0, lastpos));
621         if (num > (Long.MAX_VALUE/prefix) || num < (Long.MIN_VALUE/prefix)) {
622           throw new IllegalArgumentException(s + " does not fit in a Long");
623         }
624         return num * prefix;
625       }
626     }
627   }
628 
629     /**
630      * Escapes HTML Special characters present in the string.
631      * @param string
632      * @return HTML Escaped String representation
633      */
escapeHTML(String string)634     public static String escapeHTML(String string) {
635       if(string == null) {
636         return null;
637       }
638       StringBuffer sb = new StringBuffer();
639       boolean lastCharacterWasSpace = false;
640       char[] chars = string.toCharArray();
641       for(char c : chars) {
642         if(c == ' ') {
643           if(lastCharacterWasSpace){
644             lastCharacterWasSpace = false;
645             sb.append("&nbsp;");
646           }else {
647             lastCharacterWasSpace=true;
648             sb.append(" ");
649           }
650         }else {
651           lastCharacterWasSpace = false;
652           switch(c) {
653           case '<': sb.append("&lt;"); break;
654           case '>': sb.append("&gt;"); break;
655           case '&': sb.append("&amp;"); break;
656           case '"': sb.append("&quot;"); break;
657           default : sb.append(c);break;
658           }
659         }
660       }
661 
662       return sb.toString();
663     }
664 
665   /**
666    * Return an abbreviated English-language desc of the byte length
667    */
byteDesc(long len)668   public static String byteDesc(long len) {
669     double val = 0.0;
670     String ending = "";
671     if (len < 1024 * 1024) {
672       val = (1.0 * len) / 1024;
673       ending = " KB";
674     } else if (len < 1024 * 1024 * 1024) {
675       val = (1.0 * len) / (1024 * 1024);
676       ending = " MB";
677     } else if (len < 1024L * 1024 * 1024 * 1024) {
678       val = (1.0 * len) / (1024 * 1024 * 1024);
679       ending = " GB";
680     } else if (len < 1024L * 1024 * 1024 * 1024 * 1024) {
681       val = (1.0 * len) / (1024L * 1024 * 1024 * 1024);
682       ending = " TB";
683     } else {
684       val = (1.0 * len) / (1024L * 1024 * 1024 * 1024 * 1024);
685       ending = " PB";
686     }
687     return limitDecimalTo2(val) + ending;
688   }
689 
limitDecimalTo2(double d)690   public static synchronized String limitDecimalTo2(double d) {
691     return decimalFormat.format(d);
692   }
693 
694   /**
695    * Concatenates strings, using a separator.
696    *
697    * @param separator Separator to join with.
698    * @param strings Strings to join.
699    * @return  the joined string
700    */
join(CharSequence separator, Iterable<String> strings)701   public static String join(CharSequence separator, Iterable<String> strings) {
702     StringBuilder sb = new StringBuilder();
703     boolean first = true;
704     for (String s : strings) {
705       if (first) {
706         first = false;
707       } else {
708         sb.append(separator);
709       }
710       sb.append(s);
711     }
712     return sb.toString();
713   }
714 
715   /**
716    * Concatenates strings, using a separator.
717    *
718    * @param separator to join with
719    * @param strings to join
720    * @return  the joined string
721    */
join(CharSequence separator, String[] strings)722   public static String join(CharSequence separator, String[] strings) {
723     // Ideally we don't have to duplicate the code here if array is iterable.
724     StringBuilder sb = new StringBuilder();
725     boolean first = true;
726     for (String s : strings) {
727       if (first) {
728         first = false;
729       } else {
730         sb.append(separator);
731       }
732       sb.append(s);
733     }
734     return sb.toString();
735   }
736 
737   /**
738    * Concatenates objects, using a separator.
739    *
740    * @param separator to join with
741    * @param objects to join
742    * @return the joined string
743    */
join(CharSequence separator, Object[] objects)744   public static String join(CharSequence separator, Object[] objects) {
745     StringBuilder sb = new StringBuilder();
746     boolean first = true;
747     for (Object obj : objects) {
748       if (first) {
749         first = false;
750       } else {
751         sb.append(separator);
752       }
753       sb.append(obj);
754     }
755     return sb.toString();
756   }
757 
758   /**
759    * Capitalize a word
760    *
761    * @param s the input string
762    * @return capitalized string
763    */
capitalize(String s)764   public static String capitalize(String s) {
765     int len = s.length();
766     if (len == 0) return s;
767     return new StringBuilder(len).append(Character.toTitleCase(s.charAt(0)))
768                                  .append(s.substring(1)).toString();
769   }
770 
771   /**
772    * Convert SOME_STUFF to SomeStuff
773    *
774    * @param s input string
775    * @return camelized string
776    */
camelize(String s)777   public static String camelize(String s) {
778     StringBuilder sb = new StringBuilder();
779     String[] words = split(s.toLowerCase(Locale.US), ESCAPE_CHAR, '_');
780 
781     for (String word : words)
782       sb.append(capitalize(word));
783 
784     return sb.toString();
785   }
786 }
787