Home | History | Annotate | Download | only in util
      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 
    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 (at) 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 (at) 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 (at) 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 (at) jivesoftware.com/Smack", "matt (at) 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 (at) 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 (at) 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 }
    801