blob: 7e3cfdc739672e5a19f74de0a269792b4e09dcc7 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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
21package org.jivesoftware.smack.util;
22
23import java.io.UnsupportedEncodingException;
24import java.security.MessageDigest;
25import java.security.NoSuchAlgorithmException;
26import java.text.DateFormat;
27import java.text.ParseException;
28import java.text.SimpleDateFormat;
29import java.util.ArrayList;
30import java.util.Calendar;
31import java.util.Collections;
32import java.util.Comparator;
33import java.util.Date;
34import java.util.List;
35import java.util.Random;
36import java.util.TimeZone;
37import java.util.regex.Matcher;
38import java.util.regex.Pattern;
39
40/**
41 * A collection of utility methods for String objects.
42 */
43public 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
103 couplings.add(new PatternCouplings(datePattern, dateFormatter));
104 couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter, true));
105 couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter, true));
106 couplings.add(new PatternCouplings(timePattern, timeFormatter, true));
107 couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter));
108 couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter, true));
109 couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter));
110 }
111
112 private static final char[] QUOTE_ENCODE = "&quot;".toCharArray();
113 private static final char[] APOS_ENCODE = "&apos;".toCharArray();
114 private static final char[] AMP_ENCODE = "&amp;".toCharArray();
115 private static final char[] LT_ENCODE = "&lt;".toCharArray();
116 private static final char[] GT_ENCODE = "&gt;".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 */
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 */
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 */
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
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
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
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 */
262 public static String formatXEP0082Date(Date date) {
263 synchronized (dateTimeFormatter) {
264 return dateTimeFormatter.format(date);
265 }
266 }
267
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 */
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 */
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 */
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 */
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 */
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>&lt;space&gt;</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>&lt;</td><td>\3c</td></tr>
393 * <tr><td>&gt;</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 &lt;space&gt; 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 */
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>&lt;space&gt;</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>&lt;</td><td>\3c</td></tr>
454 * <tr><td>&gt;</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 &lt;space&gt; 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 */
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 */
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 &#235; (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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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
766 private StringUtils() {
767 // Not instantiable.
768 }
769
770 private static class PatternCouplings {
771 Pattern pattern;
772 DateFormat formatter;
773 boolean needToConvertTimeZone = false;
774
775 public PatternCouplings(Pattern datePattern, DateFormat dateFormat) {
776 pattern = datePattern;
777 formatter = dateFormat;
778 }
779
780 public PatternCouplings(Pattern datePattern, DateFormat dateFormat, boolean shouldConvertToRFC822) {
781 pattern = datePattern;
782 formatter = dateFormat;
783 needToConvertTimeZone = shouldConvertToRFC822;
784 }
785
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}