StringUtils.java revision d7955ce24d294fb2014c59d11fca184471056f44
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
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}
801