1 /** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2007 Jive Software. 7 * 8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 21 package org.jivesoftware.smack.util; 22 23 import java.io.UnsupportedEncodingException; 24 import java.security.MessageDigest; 25 import java.security.NoSuchAlgorithmException; 26 import java.text.DateFormat; 27 import java.text.ParseException; 28 import java.text.SimpleDateFormat; 29 import java.util.ArrayList; 30 import java.util.Calendar; 31 import java.util.Collections; 32 import java.util.Comparator; 33 import java.util.Date; 34 import java.util.List; 35 import java.util.Random; 36 import java.util.TimeZone; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 /** 41 * A collection of utility methods for String objects. 42 */ 43 public class StringUtils { 44 45 /** 46 * Date format as defined in XEP-0082 - XMPP Date and Time Profiles. The time zone is set to 47 * UTC. 48 * <p> 49 * Date formats are not synchronized. Since multiple threads access the format concurrently, it 50 * must be synchronized externally or you can use the convenience methods 51 * {@link #parseXEP0082Date(String)} and {@link #formatXEP0082Date(Date)}. 52 * @deprecated This public version will be removed in favor of using the methods defined within this class. 53 */ 54 public static final DateFormat XEP_0082_UTC_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); 55 56 /* 57 * private version to use internally so we don't have to be concerned with thread safety. 58 */ 59 private static final DateFormat dateFormatter = DateFormatType.XEP_0082_DATE_PROFILE.createFormatter(); 60 private static final Pattern datePattern = Pattern.compile("^\\d+-\\d+-\\d+$"); 61 62 private static final DateFormat timeFormatter = DateFormatType.XEP_0082_TIME_MILLIS_ZONE_PROFILE.createFormatter(); 63 private static final Pattern timePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$"); 64 private static final DateFormat timeNoZoneFormatter = DateFormatType.XEP_0082_TIME_MILLIS_PROFILE.createFormatter(); 65 private static final Pattern timeNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+$"); 66 67 private static final DateFormat timeNoMillisFormatter = DateFormatType.XEP_0082_TIME_ZONE_PROFILE.createFormatter(); 68 private static final Pattern timeNoMillisPattern = Pattern.compile("^(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$"); 69 private static final DateFormat timeNoMillisNoZoneFormatter = DateFormatType.XEP_0082_TIME_PROFILE.createFormatter(); 70 private static final Pattern timeNoMillisNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+$"); 71 72 private static final DateFormat dateTimeFormatter = DateFormatType.XEP_0082_DATETIME_MILLIS_PROFILE.createFormatter(); 73 private static final Pattern dateTimePattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))?$"); 74 private static final DateFormat dateTimeNoMillisFormatter = DateFormatType.XEP_0082_DATETIME_PROFILE.createFormatter(); 75 private static final Pattern dateTimeNoMillisPattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))?$"); 76 77 private static final DateFormat xep0091Formatter = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); 78 private static final DateFormat xep0091Date6DigitFormatter = new SimpleDateFormat("yyyyMd'T'HH:mm:ss"); 79 private static final DateFormat xep0091Date7Digit1MonthFormatter = new SimpleDateFormat("yyyyMdd'T'HH:mm:ss"); 80 private static final DateFormat xep0091Date7Digit2MonthFormatter = new SimpleDateFormat("yyyyMMd'T'HH:mm:ss"); 81 private static final Pattern xep0091Pattern = Pattern.compile("^\\d+T\\d+:\\d+:\\d+$"); 82 83 private static final List<PatternCouplings> couplings = new ArrayList<PatternCouplings>(); 84 85 static { 86 TimeZone utc = TimeZone.getTimeZone("UTC"); 87 XEP_0082_UTC_FORMAT.setTimeZone(utc); 88 dateFormatter.setTimeZone(utc); 89 timeFormatter.setTimeZone(utc); 90 timeNoZoneFormatter.setTimeZone(utc); 91 timeNoMillisFormatter.setTimeZone(utc); 92 timeNoMillisNoZoneFormatter.setTimeZone(utc); 93 dateTimeFormatter.setTimeZone(utc); 94 dateTimeNoMillisFormatter.setTimeZone(utc); 95 96 xep0091Formatter.setTimeZone(utc); 97 xep0091Date6DigitFormatter.setTimeZone(utc); 98 xep0091Date7Digit1MonthFormatter.setTimeZone(utc); 99 xep0091Date7Digit1MonthFormatter.setLenient(false); 100 xep0091Date7Digit2MonthFormatter.setTimeZone(utc); 101 xep0091Date7Digit2MonthFormatter.setLenient(false); 102 couplings.add(new PatternCouplings(datePattern, dateFormatter))103 couplings.add(new PatternCouplings(datePattern, dateFormatter)); couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter, true))104 couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter, true)); couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter, true))105 couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter, true)); couplings.add(new PatternCouplings(timePattern, timeFormatter, true))106 couplings.add(new PatternCouplings(timePattern, timeFormatter, true)); couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter))107 couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter)); couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter, true))108 couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter, true)); couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter))109 couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter)); 110 } 111 112 private static final char[] QUOTE_ENCODE = """.toCharArray(); 113 private static final char[] APOS_ENCODE = "'".toCharArray(); 114 private static final char[] AMP_ENCODE = "&".toCharArray(); 115 private static final char[] LT_ENCODE = "<".toCharArray(); 116 private static final char[] GT_ENCODE = ">".toCharArray(); 117 118 /** 119 * Parses the given date string in the <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a>. 120 * 121 * @param dateString the date string to parse 122 * @return the parsed Date 123 * @throws ParseException if the specified string cannot be parsed 124 * @deprecated Use {@link #parseDate(String)} instead. 125 * 126 */ parseXEP0082Date(String dateString)127 public static Date parseXEP0082Date(String dateString) throws ParseException { 128 return parseDate(dateString); 129 } 130 131 /** 132 * Parses the given date string in either of the three profiles of <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a> 133 * or <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091 - Legacy Delayed Delivery</a> format. 134 * <p> 135 * This method uses internal date formatters and is thus threadsafe. 136 * @param dateString the date string to parse 137 * @return the parsed Date 138 * @throws ParseException if the specified string cannot be parsed 139 */ parseDate(String dateString)140 public static Date parseDate(String dateString) throws ParseException { 141 Matcher matcher = xep0091Pattern.matcher(dateString); 142 143 /* 144 * if date is in XEP-0091 format handle ambiguous dates missing the 145 * leading zero in month and day 146 */ 147 if (matcher.matches()) { 148 int length = dateString.split("T")[0].length(); 149 150 if (length < 8) { 151 Date date = handleDateWithMissingLeadingZeros(dateString, length); 152 153 if (date != null) 154 return date; 155 } 156 else { 157 synchronized (xep0091Formatter) { 158 return xep0091Formatter.parse(dateString); 159 } 160 } 161 } 162 else { 163 for (PatternCouplings coupling : couplings) { 164 matcher = coupling.pattern.matcher(dateString); 165 166 if (matcher.matches()) 167 { 168 if (coupling.needToConvertTimeZone) { 169 dateString = coupling.convertTime(dateString); 170 } 171 172 synchronized (coupling.formatter) { 173 return coupling.formatter.parse(dateString); 174 } 175 } 176 } 177 } 178 179 /* 180 * We assume it is the XEP-0082 DateTime profile with no milliseconds at this point. If it isn't, is is just not parseable, then we attempt 181 * to parse it regardless and let it throw the ParseException. 182 */ 183 synchronized (dateTimeNoMillisFormatter) { 184 return dateTimeNoMillisFormatter.parse(dateString); 185 } 186 } 187 188 /** 189 * Parses the given date string in different ways and returns the date that 190 * lies in the past and/or is nearest to the current date-time. 191 * 192 * @param stampString date in string representation 193 * @param dateLength 194 * @param noFuture 195 * @return the parsed date 196 * @throws ParseException The date string was of an unknown format 197 */ handleDateWithMissingLeadingZeros(String stampString, int dateLength)198 private static Date handleDateWithMissingLeadingZeros(String stampString, int dateLength) throws ParseException { 199 if (dateLength == 6) { 200 synchronized (xep0091Date6DigitFormatter) { 201 return xep0091Date6DigitFormatter.parse(stampString); 202 } 203 } 204 Calendar now = Calendar.getInstance(); 205 206 Calendar oneDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit1MonthFormatter); 207 Calendar twoDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit2MonthFormatter); 208 209 List<Calendar> dates = filterDatesBefore(now, oneDigitMonth, twoDigitMonth); 210 211 if (!dates.isEmpty()) { 212 return determineNearestDate(now, dates).getTime(); 213 } 214 return null; 215 } 216 parseXEP91Date(String stampString, DateFormat dateFormat)217 private static Calendar parseXEP91Date(String stampString, DateFormat dateFormat) { 218 try { 219 synchronized (dateFormat) { 220 dateFormat.parse(stampString); 221 return dateFormat.getCalendar(); 222 } 223 } 224 catch (ParseException e) { 225 return null; 226 } 227 } 228 filterDatesBefore(Calendar now, Calendar... dates)229 private static List<Calendar> filterDatesBefore(Calendar now, Calendar... dates) { 230 List<Calendar> result = new ArrayList<Calendar>(); 231 232 for (Calendar calendar : dates) { 233 if (calendar != null && calendar.before(now)) { 234 result.add(calendar); 235 } 236 } 237 238 return result; 239 } 240 determineNearestDate(final Calendar now, List<Calendar> dates)241 private static Calendar determineNearestDate(final Calendar now, List<Calendar> dates) { 242 243 Collections.sort(dates, new Comparator<Calendar>() { 244 245 public int compare(Calendar o1, Calendar o2) { 246 Long diff1 = new Long(now.getTimeInMillis() - o1.getTimeInMillis()); 247 Long diff2 = new Long(now.getTimeInMillis() - o2.getTimeInMillis()); 248 return diff1.compareTo(diff2); 249 } 250 251 }); 252 253 return dates.get(0); 254 } 255 256 /** 257 * Formats a Date into a XEP-0082 - XMPP Date and Time Profiles string. 258 * 259 * @param date the time value to be formatted into a time string 260 * @return the formatted time string in XEP-0082 format 261 */ formatXEP0082Date(Date date)262 public static String formatXEP0082Date(Date date) { 263 synchronized (dateTimeFormatter) { 264 return dateTimeFormatter.format(date); 265 } 266 } 267 formatDate(Date toFormat, DateFormatType type)268 public static String formatDate(Date toFormat, DateFormatType type) 269 { 270 return null; 271 } 272 273 /** 274 * Returns the name portion of a XMPP address. For example, for the 275 * address "matt@jivesoftware.com/Smack", "matt" would be returned. If no 276 * username is present in the address, the empty string will be returned. 277 * 278 * @param XMPPAddress the XMPP address. 279 * @return the name portion of the XMPP address. 280 */ parseName(String XMPPAddress)281 public static String parseName(String XMPPAddress) { 282 if (XMPPAddress == null) { 283 return null; 284 } 285 int atIndex = XMPPAddress.lastIndexOf("@"); 286 if (atIndex <= 0) { 287 return ""; 288 } 289 else { 290 return XMPPAddress.substring(0, atIndex); 291 } 292 } 293 294 /** 295 * Returns the server portion of a XMPP address. For example, for the 296 * address "matt@jivesoftware.com/Smack", "jivesoftware.com" would be returned. 297 * If no server is present in the address, the empty string will be returned. 298 * 299 * @param XMPPAddress the XMPP address. 300 * @return the server portion of the XMPP address. 301 */ parseServer(String XMPPAddress)302 public static String parseServer(String XMPPAddress) { 303 if (XMPPAddress == null) { 304 return null; 305 } 306 int atIndex = XMPPAddress.lastIndexOf("@"); 307 // If the String ends with '@', return the empty string. 308 if (atIndex + 1 > XMPPAddress.length()) { 309 return ""; 310 } 311 int slashIndex = XMPPAddress.indexOf("/"); 312 if (slashIndex > 0 && slashIndex > atIndex) { 313 return XMPPAddress.substring(atIndex + 1, slashIndex); 314 } 315 else { 316 return XMPPAddress.substring(atIndex + 1); 317 } 318 } 319 320 /** 321 * Returns the resource portion of a XMPP address. For example, for the 322 * address "matt@jivesoftware.com/Smack", "Smack" would be returned. If no 323 * resource is present in the address, the empty string will be returned. 324 * 325 * @param XMPPAddress the XMPP address. 326 * @return the resource portion of the XMPP address. 327 */ parseResource(String XMPPAddress)328 public static String parseResource(String XMPPAddress) { 329 if (XMPPAddress == null) { 330 return null; 331 } 332 int slashIndex = XMPPAddress.indexOf("/"); 333 if (slashIndex + 1 > XMPPAddress.length() || slashIndex < 0) { 334 return ""; 335 } 336 else { 337 return XMPPAddress.substring(slashIndex + 1); 338 } 339 } 340 341 /** 342 * Returns the XMPP address with any resource information removed. For example, 343 * for the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would 344 * be returned. 345 * 346 * @param XMPPAddress the XMPP address. 347 * @return the bare XMPP address without resource information. 348 */ parseBareAddress(String XMPPAddress)349 public static String parseBareAddress(String XMPPAddress) { 350 if (XMPPAddress == null) { 351 return null; 352 } 353 int slashIndex = XMPPAddress.indexOf("/"); 354 if (slashIndex < 0) { 355 return XMPPAddress; 356 } 357 else if (slashIndex == 0) { 358 return ""; 359 } 360 else { 361 return XMPPAddress.substring(0, slashIndex); 362 } 363 } 364 365 /** 366 * Returns true if jid is a full JID (i.e. a JID with resource part). 367 * 368 * @param jid 369 * @return true if full JID, false otherwise 370 */ isFullJID(String jid)371 public static boolean isFullJID(String jid) { 372 if (parseName(jid).length() <= 0 || parseServer(jid).length() <= 0 373 || parseResource(jid).length() <= 0) { 374 return false; 375 } 376 return true; 377 } 378 379 /** 380 * Escapes the node portion of a JID according to "JID Escaping" (JEP-0106). 381 * Escaping replaces characters prohibited by node-prep with escape sequences, 382 * as follows:<p> 383 * 384 * <table border="1"> 385 * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> 386 * <tr><td><space></td><td>\20</td></tr> 387 * <tr><td>"</td><td>\22</td></tr> 388 * <tr><td>&</td><td>\26</td></tr> 389 * <tr><td>'</td><td>\27</td></tr> 390 * <tr><td>/</td><td>\2f</td></tr> 391 * <tr><td>:</td><td>\3a</td></tr> 392 * <tr><td><</td><td>\3c</td></tr> 393 * <tr><td>></td><td>\3e</td></tr> 394 * <tr><td>@</td><td>\40</td></tr> 395 * <tr><td>\</td><td>\5c</td></tr> 396 * </table><p> 397 * 398 * This process is useful when the node comes from an external source that doesn't 399 * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because 400 * the <space> character isn't a valid part of a node, the username should 401 * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" 402 * after case-folding, etc. has been applied).<p> 403 * 404 * All node escaping and un-escaping must be performed manually at the appropriate 405 * time; the JID class will not escape or un-escape automatically. 406 * 407 * @param node the node. 408 * @return the escaped version of the node. 409 */ escapeNode(String node)410 public static String escapeNode(String node) { 411 if (node == null) { 412 return null; 413 } 414 StringBuilder buf = new StringBuilder(node.length() + 8); 415 for (int i=0, n=node.length(); i<n; i++) { 416 char c = node.charAt(i); 417 switch (c) { 418 case '"': buf.append("\\22"); break; 419 case '&': buf.append("\\26"); break; 420 case '\'': buf.append("\\27"); break; 421 case '/': buf.append("\\2f"); break; 422 case ':': buf.append("\\3a"); break; 423 case '<': buf.append("\\3c"); break; 424 case '>': buf.append("\\3e"); break; 425 case '@': buf.append("\\40"); break; 426 case '\\': buf.append("\\5c"); break; 427 default: { 428 if (Character.isWhitespace(c)) { 429 buf.append("\\20"); 430 } 431 else { 432 buf.append(c); 433 } 434 } 435 } 436 } 437 return buf.toString(); 438 } 439 440 /** 441 * Un-escapes the node portion of a JID according to "JID Escaping" (JEP-0106).<p> 442 * Escaping replaces characters prohibited by node-prep with escape sequences, 443 * as follows:<p> 444 * 445 * <table border="1"> 446 * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> 447 * <tr><td><space></td><td>\20</td></tr> 448 * <tr><td>"</td><td>\22</td></tr> 449 * <tr><td>&</td><td>\26</td></tr> 450 * <tr><td>'</td><td>\27</td></tr> 451 * <tr><td>/</td><td>\2f</td></tr> 452 * <tr><td>:</td><td>\3a</td></tr> 453 * <tr><td><</td><td>\3c</td></tr> 454 * <tr><td>></td><td>\3e</td></tr> 455 * <tr><td>@</td><td>\40</td></tr> 456 * <tr><td>\</td><td>\5c</td></tr> 457 * </table><p> 458 * 459 * This process is useful when the node comes from an external source that doesn't 460 * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because 461 * the <space> character isn't a valid part of a node, the username should 462 * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" 463 * after case-folding, etc. has been applied).<p> 464 * 465 * All node escaping and un-escaping must be performed manually at the appropriate 466 * time; the JID class will not escape or un-escape automatically. 467 * 468 * @param node the escaped version of the node. 469 * @return the un-escaped version of the node. 470 */ unescapeNode(String node)471 public static String unescapeNode(String node) { 472 if (node == null) { 473 return null; 474 } 475 char [] nodeChars = node.toCharArray(); 476 StringBuilder buf = new StringBuilder(nodeChars.length); 477 for (int i=0, n=nodeChars.length; i<n; i++) { 478 compare: { 479 char c = node.charAt(i); 480 if (c == '\\' && i+2<n) { 481 char c2 = nodeChars[i+1]; 482 char c3 = nodeChars[i+2]; 483 if (c2 == '2') { 484 switch (c3) { 485 case '0': buf.append(' '); i+=2; break compare; 486 case '2': buf.append('"'); i+=2; break compare; 487 case '6': buf.append('&'); i+=2; break compare; 488 case '7': buf.append('\''); i+=2; break compare; 489 case 'f': buf.append('/'); i+=2; break compare; 490 } 491 } 492 else if (c2 == '3') { 493 switch (c3) { 494 case 'a': buf.append(':'); i+=2; break compare; 495 case 'c': buf.append('<'); i+=2; break compare; 496 case 'e': buf.append('>'); i+=2; break compare; 497 } 498 } 499 else if (c2 == '4') { 500 if (c3 == '0') { 501 buf.append("@"); 502 i+=2; 503 break compare; 504 } 505 } 506 else if (c2 == '5') { 507 if (c3 == 'c') { 508 buf.append("\\"); 509 i+=2; 510 break compare; 511 } 512 } 513 } 514 buf.append(c); 515 } 516 } 517 return buf.toString(); 518 } 519 520 /** 521 * Escapes all necessary characters in the String so that it can be used 522 * in an XML doc. 523 * 524 * @param string the string to escape. 525 * @return the string with appropriate characters escaped. 526 */ escapeForXML(String string)527 public static String escapeForXML(String string) { 528 if (string == null) { 529 return null; 530 } 531 char ch; 532 int i=0; 533 int last=0; 534 char[] input = string.toCharArray(); 535 int len = input.length; 536 StringBuilder out = new StringBuilder((int)(len*1.3)); 537 for (; i < len; i++) { 538 ch = input[i]; 539 if (ch > '>') { 540 } 541 else if (ch == '<') { 542 if (i > last) { 543 out.append(input, last, i - last); 544 } 545 last = i + 1; 546 out.append(LT_ENCODE); 547 } 548 else if (ch == '>') { 549 if (i > last) { 550 out.append(input, last, i - last); 551 } 552 last = i + 1; 553 out.append(GT_ENCODE); 554 } 555 556 else if (ch == '&') { 557 if (i > last) { 558 out.append(input, last, i - last); 559 } 560 // Do nothing if the string is of the form ë (unicode value) 561 if (!(len > i + 5 562 && input[i + 1] == '#' 563 && Character.isDigit(input[i + 2]) 564 && Character.isDigit(input[i + 3]) 565 && Character.isDigit(input[i + 4]) 566 && input[i + 5] == ';')) { 567 last = i + 1; 568 out.append(AMP_ENCODE); 569 } 570 } 571 else if (ch == '"') { 572 if (i > last) { 573 out.append(input, last, i - last); 574 } 575 last = i + 1; 576 out.append(QUOTE_ENCODE); 577 } 578 else if (ch == '\'') { 579 if (i > last) { 580 out.append(input, last, i - last); 581 } 582 last = i + 1; 583 out.append(APOS_ENCODE); 584 } 585 } 586 if (last == 0) { 587 return string; 588 } 589 if (i > last) { 590 out.append(input, last, i - last); 591 } 592 return out.toString(); 593 } 594 595 /** 596 * Used by the hash method. 597 */ 598 private static MessageDigest digest = null; 599 600 /** 601 * Hashes a String using the SHA-1 algorithm and returns the result as a 602 * String of hexadecimal numbers. This method is synchronized to avoid 603 * excessive MessageDigest object creation. If calling this method becomes 604 * a bottleneck in your code, you may wish to maintain a pool of 605 * MessageDigest objects instead of using this method. 606 * <p> 607 * A hash is a one-way function -- that is, given an 608 * input, an output is easily computed. However, given the output, the 609 * input is almost impossible to compute. This is useful for passwords 610 * since we can store the hash and a hacker will then have a very hard time 611 * determining the original password. 612 * 613 * @param data the String to compute the hash of. 614 * @return a hashed version of the passed-in String 615 */ hash(String data)616 public synchronized static String hash(String data) { 617 if (digest == null) { 618 try { 619 digest = MessageDigest.getInstance("SHA-1"); 620 } 621 catch (NoSuchAlgorithmException nsae) { 622 System.err.println("Failed to load the SHA-1 MessageDigest. " + 623 "Jive will be unable to function normally."); 624 } 625 } 626 // Now, compute hash. 627 try { 628 digest.update(data.getBytes("UTF-8")); 629 } 630 catch (UnsupportedEncodingException e) { 631 System.err.println(e); 632 } 633 return encodeHex(digest.digest()); 634 } 635 636 /** 637 * Encodes an array of bytes as String representation of hexadecimal. 638 * 639 * @param bytes an array of bytes to convert to a hex string. 640 * @return generated hex string. 641 */ encodeHex(byte[] bytes)642 public static String encodeHex(byte[] bytes) { 643 StringBuilder hex = new StringBuilder(bytes.length * 2); 644 645 for (byte aByte : bytes) { 646 if (((int) aByte & 0xff) < 0x10) { 647 hex.append("0"); 648 } 649 hex.append(Integer.toString((int) aByte & 0xff, 16)); 650 } 651 652 return hex.toString(); 653 } 654 655 /** 656 * Encodes a String as a base64 String. 657 * 658 * @param data a String to encode. 659 * @return a base64 encoded String. 660 */ encodeBase64(String data)661 public static String encodeBase64(String data) { 662 byte [] bytes = null; 663 try { 664 bytes = data.getBytes("ISO-8859-1"); 665 } 666 catch (UnsupportedEncodingException uee) { 667 uee.printStackTrace(); 668 } 669 return encodeBase64(bytes); 670 } 671 672 /** 673 * Encodes a byte array into a base64 String. 674 * 675 * @param data a byte array to encode. 676 * @return a base64 encode String. 677 */ encodeBase64(byte[] data)678 public static String encodeBase64(byte[] data) { 679 return encodeBase64(data, false); 680 } 681 682 /** 683 * Encodes a byte array into a bse64 String. 684 * 685 * @param data The byte arry to encode. 686 * @param lineBreaks True if the encoding should contain line breaks and false if it should not. 687 * @return A base64 encoded String. 688 */ encodeBase64(byte[] data, boolean lineBreaks)689 public static String encodeBase64(byte[] data, boolean lineBreaks) { 690 return encodeBase64(data, 0, data.length, lineBreaks); 691 } 692 693 /** 694 * Encodes a byte array into a bse64 String. 695 * 696 * @param data The byte arry to encode. 697 * @param offset the offset of the bytearray to begin encoding at. 698 * @param len the length of bytes to encode. 699 * @param lineBreaks True if the encoding should contain line breaks and false if it should not. 700 * @return A base64 encoded String. 701 */ encodeBase64(byte[] data, int offset, int len, boolean lineBreaks)702 public static String encodeBase64(byte[] data, int offset, int len, boolean lineBreaks) { 703 return Base64.encodeBytes(data, offset, len, (lineBreaks ? Base64.NO_OPTIONS : Base64.DONT_BREAK_LINES)); 704 } 705 706 /** 707 * Decodes a base64 String. 708 * Unlike Base64.decode() this method does not try to detect and decompress a gzip-compressed input. 709 * 710 * @param data a base64 encoded String to decode. 711 * @return the decoded String. 712 */ decodeBase64(String data)713 public static byte[] decodeBase64(String data) { 714 byte[] bytes; 715 try { 716 bytes = data.getBytes("UTF-8"); 717 } catch (java.io.UnsupportedEncodingException uee) { 718 bytes = data.getBytes(); 719 } 720 721 bytes = Base64.decode(bytes, 0, bytes.length, Base64.NO_OPTIONS); 722 return bytes; 723 } 724 725 /** 726 * Pseudo-random number generator object for use with randomString(). 727 * The Random class is not considered to be cryptographically secure, so 728 * only use these random Strings for low to medium security applications. 729 */ 730 private static Random randGen = new Random(); 731 732 /** 733 * Array of numbers and letters of mixed case. Numbers appear in the list 734 * twice so that there is a more equal chance that a number will be picked. 735 * We can use the array to get a random number or letter by picking a random 736 * array index. 737 */ 738 private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" + 739 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); 740 741 /** 742 * Returns a random String of numbers and letters (lower and upper case) 743 * of the specified length. The method uses the Random class that is 744 * built-in to Java which is suitable for low to medium grade security uses. 745 * This means that the output is only pseudo random, i.e., each number is 746 * mathematically generated so is not truly random.<p> 747 * 748 * The specified length must be at least one. If not, the method will return 749 * null. 750 * 751 * @param length the desired length of the random String to return. 752 * @return a random String of numbers and letters of the specified length. 753 */ randomString(int length)754 public static String randomString(int length) { 755 if (length < 1) { 756 return null; 757 } 758 // Create a char buffer to put random letters and numbers in. 759 char [] randBuffer = new char[length]; 760 for (int i=0; i<randBuffer.length; i++) { 761 randBuffer[i] = numbersAndLetters[randGen.nextInt(71)]; 762 } 763 return new String(randBuffer); 764 } 765 StringUtils()766 private StringUtils() { 767 // Not instantiable. 768 } 769 770 private static class PatternCouplings { 771 Pattern pattern; 772 DateFormat formatter; 773 boolean needToConvertTimeZone = false; 774 PatternCouplings(Pattern datePattern, DateFormat dateFormat)775 public PatternCouplings(Pattern datePattern, DateFormat dateFormat) { 776 pattern = datePattern; 777 formatter = dateFormat; 778 } 779 PatternCouplings(Pattern datePattern, DateFormat dateFormat, boolean shouldConvertToRFC822)780 public PatternCouplings(Pattern datePattern, DateFormat dateFormat, boolean shouldConvertToRFC822) { 781 pattern = datePattern; 782 formatter = dateFormat; 783 needToConvertTimeZone = shouldConvertToRFC822; 784 } 785 convertTime(String dateString)786 public String convertTime(String dateString) { 787 if (dateString.charAt(dateString.length() - 1) == 'Z') { 788 return dateString.replace("Z", "+0000"); 789 } 790 else { 791 // If the time zone wasn't specified with 'Z', then it's in 792 // ISO8601 format (i.e. '(+|-)HH:mm') 793 // RFC822 needs a similar format just without the colon (i.e. 794 // '(+|-)HHmm)'), so remove it 795 return dateString.replaceAll("([\\+\\-]\\d\\d):(\\d\\d)","$1$2"); 796 } 797 } 798 } 799 800 } 801