001    package biweekly.util;
002    
003    import java.text.DateFormat;
004    import java.text.ParseException;
005    import java.util.Date;
006    import java.util.TimeZone;
007    
008    /*
009     Copyright (c) 2013, Michael Angstadt
010     All rights reserved.
011    
012     Redistribution and use in source and binary forms, with or without
013     modification, are permitted provided that the following conditions are met: 
014    
015     1. Redistributions of source code must retain the above copyright notice, this
016     list of conditions and the following disclaimer. 
017     2. Redistributions in binary form must reproduce the above copyright notice,
018     this list of conditions and the following disclaimer in the documentation
019     and/or other materials provided with the distribution. 
020    
021     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
022     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
023     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
024     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
025     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
026     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
027     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
028     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
029     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
030     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
031     */
032    
033    /**
034     * Helper class that formats and parses iCalendar dates. iCalendar dates adhere
035     * to the ISO8601 date format standard.
036     * @author Michael Angstadt
037     */
038    public class ICalDateFormatter {
039            /**
040             * Formats a date for inclusion in an iCalendar object.
041             * @param date the date to format
042             * @param format the format to use
043             * @return the formatted date
044             */
045            public static String format(Date date, ISOFormat format) {
046                    return format(date, format, null);
047            }
048    
049            /**
050             * Formats a date for inclusion in an iCalendar object.
051             * @param date the date to format
052             * @param format the format to use
053             * @param timeZone the timezone to format the date in or null to use the
054             * JVM's default timezone (ignored with "UTC" formats)
055             * @return the formatted date
056             */
057            public static String format(Date date, ISOFormat format, TimeZone timeZone) {
058                    switch (format) {
059                    case UTC_TIME_BASIC:
060                    case UTC_TIME_EXTENDED:
061                            timeZone = TimeZone.getTimeZone("UTC");
062                            break;
063                    }
064    
065                    DateFormat df = format.getFormatDateFormat();
066                    if (timeZone != null) {
067                            df.setTimeZone(timeZone);
068                    }
069                    String str = df.format(date);
070    
071                    switch (format) {
072                    case TIME_EXTENDED:
073                            //add a colon to the timezone
074                            //example: converts "2012-07-05T22:31:41-0400" to "2012-07-05T22:31:41-04:00"
075                            str = str.replaceAll("([-\\+]\\d{2})(\\d{2})$", "$1:$2");
076                            break;
077                    }
078    
079                    return str;
080            }
081    
082            /**
083             * Parses an iCalendar date.
084             * @param dateStr the date string to parse (e.g. "20130609T181023Z")
085             * @return the parsed date
086             * @throws IllegalArgumentException if the date string isn't in one of the
087             * accepted ISO8601 formats
088             */
089            public static Date parse(String dateStr) {
090                    return parse(dateStr, null);
091            }
092    
093            /**
094             * Parses an iCalendar date.
095             * @param dateStr the date string to parse (e.g. "20130609T181023Z")
096             * @param timezone the timezone to parse the date as or null to use the
097             * JVM's default timezone (if the date string contains its own timezone,
098             * then that timezone will be used instead)
099             * @return the parsed date
100             * @throws IllegalArgumentException if the date string isn't in one of the
101             * accepted ISO8601 formats
102             */
103            public static Date parse(String dateStr, TimeZone timezone) {
104                    //find out what ISOFormat the date is in
105                    ISOFormat format = null;
106                    for (ISOFormat f : ISOFormat.values()) {
107                            if (f.matches(dateStr)) {
108                                    format = f;
109                                    break;
110                            }
111                    }
112                    if (format == null) {
113                            throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
114                    }
115    
116                    //tweak the date string to make it work with SimpleDateFormat
117                    switch (format) {
118                    case TIME_EXTENDED:
119                    case HCARD_TIME_TAG:
120                            //SimpleDateFormat doesn't recognize timezone offsets that have colons
121                            //so remove the colon from the timezone offset
122                            dateStr = dateStr.replaceAll("([-\\+]\\d{2}):(\\d{2})$", "$1$2");
123                            break;
124                    case UTC_TIME_BASIC:
125                    case UTC_TIME_EXTENDED:
126                            //SimpleDateFormat doesn't recognize "Z"
127                            dateStr = dateStr.replace("Z", "+0000");
128                            break;
129                    }
130    
131                    //parse the date
132                    DateFormat df = format.getParseDateFormat();
133                    if (timezone != null) {
134                            df.setTimeZone(timezone);
135                    }
136                    try {
137                            return df.parse(dateStr);
138                    } catch (ParseException e) {
139                            //should never be thrown because the string is checked against a regex
140                            throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
141                    }
142            }
143    
144            /**
145             * Determines whether a date string has a time component.
146             * @param dateStr the date string (e.g. "20130601T120000")
147             * @return true if it has a time component, false if not
148             */
149            public static boolean dateHasTime(String dateStr) {
150                    return dateStr.contains("T");
151            }
152    
153            /**
154             * Determines whether a date string is in UTC time or has a timezone offset.
155             * @param dateStr the date string (e.g. "20130601T120000Z",
156             * "20130601T120000-0400")
157             * @return true if it has a timezone, false if not
158             */
159            public static boolean dateHasTimezone(String dateStr) {
160                    return dateStr.endsWith("Z") || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d");
161            }
162    
163            /**
164             * Gets the {@link TimeZone} object that corresponds to the given ID.
165             * @param timezoneId the timezone ID (e.g. "America/New_York")
166             * @return the timezone object or null if not found
167             */
168            public static TimeZone parseTimeZoneId(String timezoneId) {
169                    TimeZone timezone = TimeZone.getTimeZone(timezoneId);
170                    return "GMT".equals(timezone.getID()) ? null : timezone;
171            }
172    
173            private ICalDateFormatter() {
174                    //hide constructor
175            }
176    }