001package ezvcard.util;
002
003import java.text.DateFormat;
004import java.text.FieldPosition;
005import java.text.SimpleDateFormat;
006import java.util.Calendar;
007import java.util.Date;
008import java.util.TimeZone;
009import java.util.regex.Matcher;
010import java.util.regex.Pattern;
011
012import ezvcard.Messages;
013
014/*
015 Copyright (c) 2012-2018, Michael Angstadt
016 All rights reserved.
017
018 Redistribution and use in source and binary forms, with or without
019 modification, are permitted provided that the following conditions are met: 
020
021 1. Redistributions of source code must retain the above copyright notice, this
022 list of conditions and the following disclaimer. 
023 2. Redistributions in binary form must reproduce the above copyright notice,
024 this list of conditions and the following disclaimer in the documentation
025 and/or other materials provided with the distribution. 
026
027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
030 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
031 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
032 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
033 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
034 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
035 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
036 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037
038 The views and conclusions contained in the software and documentation are those
039 of the authors and should not be interpreted as representing official policies, 
040 either expressed or implied, of the FreeBSD Project.
041 */
042
043/**
044 * Defines all of the date formats that are used in vCards, and also
045 * parses/formats vCard dates. These date formats are defined in the ISO8601
046 * specification.
047 * @author Michael Angstadt
048 */
049public enum VCardDateFormat {
050        //@formatter:off
051        /**
052         * Example: 20120701
053         */
054        DATE_BASIC(
055        "yyyyMMdd"),
056        
057        /**
058         * Example: 2012-07-01
059         */
060        DATE_EXTENDED(
061        "yyyy-MM-dd"),
062        
063        /**
064         * Example: 20120701T142110-0500
065         */
066        DATE_TIME_BASIC(
067        "yyyyMMdd'T'HHmmssZ"),
068        
069        /**
070         * Example: 2012-07-01T14:21:10-05:00
071         */
072        DATE_TIME_EXTENDED(
073        "yyyy-MM-dd'T'HH:mm:ssZ"){
074                @SuppressWarnings("serial")
075                @Override
076                public DateFormat getDateFormat(TimeZone timezone) {
077                        DateFormat df = new SimpleDateFormat(formatStr){
078                                @Override
079                                public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition){
080                                        StringBuffer sb = super.format(date, toAppendTo, fieldPosition);
081                                        
082                                        //add a colon between the hour and minute offsets
083                                        sb.insert(sb.length()-2, ':');
084                                        
085                                        return sb;
086                                }
087                        };
088                        
089                        if (timezone != null){
090                                df.setTimeZone(timezone);
091                        }
092                        
093                        return df;
094                }
095        },
096        
097        /**
098         * Example: 20120701T192110Z
099         */
100        UTC_DATE_TIME_BASIC(
101        "yyyyMMdd'T'HHmmss'Z'"){
102                @Override
103                public DateFormat getDateFormat(TimeZone timezone) {
104                        //always use the UTC timezone
105                        TimeZone utc = TimeZone.getTimeZone("UTC");
106                        return super.getDateFormat(utc);
107                }
108        },
109        
110        /**
111         * Example: 2012-07-01T19:21:10Z
112         */
113        UTC_DATE_TIME_EXTENDED(
114        "yyyy-MM-dd'T'HH:mm:ss'Z'"){
115                @Override
116                public DateFormat getDateFormat(TimeZone timezone) {
117                        //always use the UTC timezone
118                        TimeZone utc = TimeZone.getTimeZone("UTC");
119                        return super.getDateFormat(utc);
120                }
121        },
122        
123        /**
124         * Example: 2012-07-01T14:21:10-0500
125         */
126        HCARD_DATE_TIME(
127        "yyyy-MM-dd'T'HH:mm:ssZ")
128        
129        ;
130        //@formatter:on
131
132        /**
133         * The {@link SimpleDateFormat} format string used for parsing dates.
134         */
135        protected final String formatStr;
136
137        /**
138         * @param formatStr the {@link SimpleDateFormat} format string used for
139         * formatting dates.
140         */
141        private VCardDateFormat(String formatStr) {
142                this.formatStr = formatStr;
143        }
144
145        /**
146         * Builds a {@link DateFormat} object for parsing and formating dates in
147         * this format.
148         * @return the {@link DateFormat} object
149         */
150        public DateFormat getDateFormat() {
151                return getDateFormat(null);
152        }
153
154        /**
155         * Builds a {@link DateFormat} object for parsing and formating dates in
156         * this format.
157         * @param timezone the timezone the date is in or null for the default
158         * timezone
159         * @return the {@link DateFormat} object
160         */
161        public DateFormat getDateFormat(TimeZone timezone) {
162                DateFormat df = new SimpleDateFormat(formatStr);
163                if (timezone != null) {
164                        df.setTimeZone(timezone);
165                }
166                return df;
167        }
168
169        /**
170         * Formats a date in this vCard date format.
171         * @param date the date to format
172         * @return the date string
173         */
174        public String format(Date date) {
175                return format(date, null);
176        }
177
178        /**
179         * Formats a date in this vCard date format.
180         * @param date the date to format
181         * @param timezone the timezone to format the date in or null for the
182         * default timezone
183         * @return the date string
184         */
185        public String format(Date date, TimeZone timezone) {
186                DateFormat df = getDateFormat(timezone);
187                return df.format(date);
188        }
189
190        /**
191         * Parses a date string.
192         * @param dateStr the date string to parse (e.g. "20130609T181023Z")
193         * @return the parsed date
194         * @throws IllegalArgumentException if the date string isn't in one of the
195         * accepted ISO8601 formats
196         */
197        public static Date parse(String dateStr) {
198                TimestampPattern p = new TimestampPattern(dateStr);
199                if (!p.matches()) {
200                        throw Messages.INSTANCE.getIllegalArgumentException(41, dateStr);
201                }
202
203                TimeZone timezone = p.hasOffset() ? TimeZone.getTimeZone("UTC") : TimeZone.getDefault();
204                Calendar c = Calendar.getInstance(timezone);
205                c.clear();
206
207                c.set(Calendar.YEAR, p.year());
208                c.set(Calendar.MONTH, p.month() - 1);
209                c.set(Calendar.DATE, p.date());
210
211                if (p.hasTime()) {
212                        c.set(Calendar.HOUR_OF_DAY, p.hour());
213                        c.set(Calendar.MINUTE, p.minute());
214                        c.set(Calendar.SECOND, p.second());
215                        c.set(Calendar.MILLISECOND, p.millisecond());
216
217                        if (p.hasOffset()) {
218                                c.set(Calendar.ZONE_OFFSET, p.offsetMillis());
219                        }
220                }
221
222                return c.getTime();
223        }
224
225        /**
226         * Wrapper for a complex regular expression that parses multiple date
227         * formats.
228         */
229        private static class TimestampPattern {
230                //@formatter:off
231                private static final Pattern regex = Pattern.compile(
232                        "^(\\d{4})-?(\\d{2})-?(\\d{2})" +
233                        "(" +
234                                "T(\\d{2}):?(\\d{2}):?(\\d{2})(\\.\\d+)?" +
235                                "(" +
236                                        "Z|([-+])((\\d{2})|((\\d{2}):?(\\d{2})))" +
237                                ")?" +
238                        ")?$"
239                );
240                //@formatter:on
241
242                private final Matcher m;
243                private final boolean matches;
244
245                public TimestampPattern(String str) {
246                        m = regex.matcher(str);
247                        matches = m.find();
248                }
249
250                public boolean matches() {
251                        return matches;
252                }
253
254                public int year() {
255                        return parseInt(1);
256                }
257
258                public int month() {
259                        return parseInt(2);
260                }
261
262                public int date() {
263                        return parseInt(3);
264                }
265
266                public boolean hasTime() {
267                        return m.group(5) != null;
268                }
269
270                public int hour() {
271                        return parseInt(5);
272                }
273
274                public int minute() {
275                        return parseInt(6);
276                }
277
278                public int second() {
279                        return parseInt(7);
280                }
281
282                public int millisecond() {
283                        if (m.group(8) == null) {
284                                return 0;
285                        }
286
287                        double ms = Double.parseDouble(m.group(8)) * 1000;
288                        return (int) Math.round(ms);
289                }
290
291                public boolean hasOffset() {
292                        return m.group(9) != null;
293                }
294
295                public int offsetMillis() {
296                        if (m.group(9).equals("Z")) {
297                                return 0;
298                        }
299
300                        int positive = m.group(10).equals("+") ? 1 : -1;
301
302                        int offsetHour, offsetMinute;
303                        if (m.group(12) != null) {
304                                offsetHour = parseInt(12);
305                                offsetMinute = 0;
306                        } else {
307                                offsetHour = parseInt(14);
308                                offsetMinute = parseInt(15);
309                        }
310
311                        return (offsetHour * 60 * 60 * 1000 + offsetMinute * 60 * 1000) * positive;
312                }
313
314                private int parseInt(int group) {
315                        return Integer.parseInt(m.group(group));
316                }
317        }
318
319        /**
320         * Determines whether a date string has a time component.
321         * @param dateStr the date string (e.g. "20130601T120000")
322         * @return true if it has a time component, false if not
323         */
324        public static boolean dateHasTime(String dateStr) {
325                return dateStr.contains("T");
326        }
327
328        /**
329         * Determines whether a date string is in UTC time or has a timezone offset.
330         * @param dateStr the date string (e.g. "20130601T120000Z",
331         * "20130601T120000-0400")
332         * @return true if it has a timezone, false if not
333         */
334        public static boolean dateHasTimezone(String dateStr) {
335                return dateStr.endsWith("Z") || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d");
336        }
337
338        /**
339         * Gets the {@link TimeZone} object that corresponds to the given ID.
340         * @param timezoneId the timezone ID (e.g. "America/New_York")
341         * @return the timezone object or null if not found
342         */
343        public static TimeZone parseTimeZoneId(String timezoneId) {
344                TimeZone timezone = TimeZone.getTimeZone(timezoneId);
345                return "GMT".equals(timezone.getID()) ? null : timezone;
346        }
347}