001    package ezvcard.util;
002    
003    import java.text.DateFormat;
004    import java.text.ParseException;
005    import java.util.Date;
006    import java.util.TimeZone;
007    import java.util.regex.Matcher;
008    import java.util.regex.Pattern;
009    
010    /*
011     Copyright (c) 2012, Michael Angstadt
012     All rights reserved.
013    
014     Redistribution and use in source and binary forms, with or without
015     modification, are permitted provided that the following conditions are met: 
016    
017     1. Redistributions of source code must retain the above copyright notice, this
018     list of conditions and the following disclaimer. 
019     2. Redistributions in binary form must reproduce the above copyright notice,
020     this list of conditions and the following disclaimer in the documentation
021     and/or other materials provided with the distribution. 
022    
023     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
024     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
025     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
026     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
027     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
028     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
029     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
030     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
032     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033    
034     The views and conclusions contained in the software and documentation are those
035     of the authors and should not be interpreted as representing official policies, 
036     either expressed or implied, of the FreeBSD Project.
037     */
038    
039    /**
040     * Helper class that formats and parses vCard dates. vCard dates adhere to the
041     * ISO8601 date format standard.
042     * @author Michael Angstadt
043     */
044    public class VCardDateFormatter {
045            /**
046             * Regular expression used to parse timezone offset strings.
047             */
048            private static final Pattern timeZoneRegex = Pattern.compile("^([-\\+])?(\\d{1,2})(:?(\\d{2}))?$");
049    
050            /**
051             * Formats a date for inclusion in a vCard.
052             * @param date the date to format
053             * @param format the format to use
054             * @return the formatted date
055             */
056            public static String format(Date date, ISOFormat format) {
057                    return format(date, format, TimeZone.getDefault());
058            }
059    
060            /**
061             * Formats a date for inclusion in a vCard.
062             * @param date the date to format
063             * @param format the format to use
064             * @param timeZone the time zone to format the date in. This will be ignored
065             * if the specified ISOFormat is a "UTC" format
066             * @return the formatted date
067             */
068            public static String format(Date date, ISOFormat format, TimeZone timeZone) {
069                    switch (format) {
070                    case UTC_TIME_BASIC:
071                    case UTC_TIME_EXTENDED:
072                            timeZone = TimeZone.getTimeZone("UTC");
073                            break;
074                    }
075    
076                    DateFormat df = format.getFormatDateFormat();
077                    df.setTimeZone(timeZone);
078                    String str = df.format(date);
079    
080                    switch (format) {
081                    case TIME_EXTENDED:
082                            //add a colon to the timezone
083                            //example: converts "2012-07-05T22:31:41-0400" to "2012-07-05T22:31:41-04:00"
084                            str = str.replaceAll("([-\\+]\\d{2})(\\d{2})$", "$1:$2");
085                            break;
086                    }
087    
088                    return str;
089            }
090    
091            /**
092             * Parses a vCard date.
093             * @param dateStr the date string to parse
094             * @return the parsed date
095             * @throws IllegalArgumentException if the date string isn't in one of the
096             * accepted ISO8601 formats
097             */
098            public static Date parse(String dateStr) {
099                    //find out what ISOFormat the date is in
100                    ISOFormat format = null;
101                    for (ISOFormat f : ISOFormat.values()) {
102                            if (f.matches(dateStr)) {
103                                    format = f;
104                                    break;
105                            }
106                    }
107                    if (format == null) {
108                            throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
109                    }
110    
111                    //tweak the date string to make it work with SimpleDateFormat
112                    switch (format) {
113                    case TIME_EXTENDED:
114                    case HCARD_TIME_TAG:
115                            //SimpleDateFormat doesn't recognize timezone offsets that have colons
116                            //so remove the colon from the timezone offset
117                            dateStr = dateStr.replaceAll("([-\\+]\\d{2}):(\\d{2})$", "$1$2");
118                            break;
119                    case UTC_TIME_BASIC:
120                    case UTC_TIME_EXTENDED:
121                            //SimpleDateFormat doesn't recognize "Z"
122                            dateStr = dateStr.replace("Z", "+0000");
123                            break;
124                    }
125    
126                    //parse the date
127                    DateFormat df = format.getParseDateFormat();
128                    try {
129                            return df.parse(dateStr);
130                    } catch (ParseException e) {
131                            //should never be thrown because the string is checked against a regex
132                            return null;
133                    }
134            }
135    
136            /**
137             * Parses a timezone that's in ISO8601 format.
138             * @param offsetStr the timezone offset string (e.g. "-0500" or "-05:00")
139             * @return the hour offset (index 0) and the minute offset (index 1)
140             * @throws IllegalArgumentException if the timezone string isn't in the
141             * right format
142             */
143            public static int[] parseTimeZone(String offsetStr) {
144                    Matcher m = timeZoneRegex.matcher(offsetStr);
145    
146                    if (!m.find()) {
147                            throw new IllegalArgumentException("Offset string is not in ISO8610 format: " + offsetStr);
148                    }
149    
150                    String sign = m.group(1);
151                    boolean positive;
152                    if ("-".equals(sign)) {
153                            positive = false;
154                    } else {
155                            positive = true;
156                    }
157    
158                    String hourStr = m.group(2);
159                    int hourOffset = Integer.parseInt(hourStr);
160                    if (!positive) {
161                            hourOffset *= -1;
162                    }
163    
164                    String minuteStr = m.group(4);
165                    int minuteOffset = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr);
166    
167                    return new int[] { hourOffset, minuteOffset };
168            }
169    
170            /**
171             * Formats a {@link TimeZone} object according to ISO8601 rules.
172             * 
173             * @param timeZone the timezone to format
174             * @param extended true to use "extended" format, false not to. Extended
175             * format will put a colon between the hour and minute.
176             * @return the formatted timezone (e.g. "+0530" or "+05:30")
177             */
178            public static String formatTimeZone(TimeZone timeZone, boolean extended) {
179                    int hours = timeZone.getRawOffset() / 1000 / 60 / 60;
180                    int minutes = Math.abs((timeZone.getRawOffset() / 1000) / 60) % 60;
181                    return formatTimeZone(hours, minutes, extended);
182            }
183    
184            /**
185             * Formats a timezone offset according to ISO8601 rules.
186             * 
187             * @param hourOffset the hour offset
188             * @param minuteOffset the minute offset (must be between 0 and 59)
189             * @param extended true to use "extended" format, false not to. Extended
190             * format will put a colon between the hour and minute.
191             * @return the formatted timezone (e.g. "+0530" or "+05:30")
192             */
193            public static String formatTimeZone(int hourOffset, int minuteOffset, boolean extended) {
194                    if (minuteOffset < 0 || minuteOffset > 59) {
195                            throw new IllegalArgumentException("Minute offset must be between 0 and 59.");
196                    }
197    
198                    StringBuilder sb = new StringBuilder();
199                    boolean positive = hourOffset >= 0;
200    
201                    sb.append(positive ? '+' : '-');
202    
203                    hourOffset = Math.abs(hourOffset);
204                    if (hourOffset < 10) {
205                            sb.append('0');
206                    }
207                    sb.append(hourOffset);
208    
209                    if (extended) {
210                            sb.append(':');
211                    }
212    
213                    if (minuteOffset < 10) {
214                            sb.append('0');
215                    }
216                    sb.append(minuteOffset);
217    
218                    return sb.toString();
219            }
220    
221            private VCardDateFormatter() {
222                    //hide constructor
223            }
224    }