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 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(" "); 743 }else { 744 lastCharacterWasSpace=true; 745 sb.append(" "); 746 } 747 }else { 748 lastCharacterWasSpace = false; 749 switch(c) { 750 case '<': sb.append("<"); break; 751 case '>': sb.append(">"); break; 752 case '&': sb.append("&"); break; 753 case '"': sb.append("""); 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}