001package co.codewizards.cloudstore.core.util;
002
003import java.util.Calendar;
004import java.util.Date;
005import java.util.GregorianCalendar;
006import java.util.Locale;
007import java.util.TimeZone;
008
009/**
010 * The <code>ISO8601</code> utility class provides helper methods
011 * to deal with date/time formatting using a specific ISO8601-compliant
012 * format (see <a href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>).
013 * <p/>
014 * The currently supported format is:
015 * <pre>
016 *   &plusmn;YYYY-MM-DDThh:mm:ss.SSSTZD
017 * </pre>
018 * where:
019 * <pre>
020 *   &plusmn;YYYY = four-digit year with optional sign where values <= 0 are
021 *           denoting years BCE and values > 0 are denoting years CE,
022 *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
023 *           0001 denotes the year 1 CE, and so on...
024 *   MM    = two-digit month (01=January, etc.)
025 *   DD    = two-digit day of month (01 through 31)
026 *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
027 *   mm    = two digits of minute (00 through 59)
028 *   ss    = two digits of second (00 through 59)
029 *   SSS   = three digits of milliseconds (000 through 999)
030 *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
031 *           in the form of +hh:mm or -hh:mm
032 * </pre>
033 * <p>
034 * This class was copied from the
035 * <a href="http://svn.apache.org/repos/asf/jackrabbit/trunk/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ISO8601.java">Apache SVN</a>.
036 * It is free software under the Apache Licence.
037 */
038public final class ISO8601 {
039
040        /**
041         * Convenience method calling {@link #parse(String)} and converting the
042         * result to a {@link Date}.
043         * @param text the text to be parsed. Must not be <code>null</code>.
044         * @return the parsed {@link Date}. Never <code>null</code>.
045         */
046        public static Date parseDate(String text) {
047                Calendar calendar = parse(text);
048                return calendar.getTime();
049        }
050
051        /**
052         * Convenience method calling {@link #format(Calendar)} with a UTC
053         * time zone (as a {@link Date} does not have a time zone by definition).
054         */
055        public static String formatDate(Date date) throws IllegalArgumentException {
056                if (date == null) {
057                        throw new IllegalArgumentException("argument can not be null");
058                }
059                Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.UK);
060                calendar.setTime(date);
061                return format(calendar);
062        }
063
064        /**
065         * Parses an ISO8601-compliant date/time string.
066         *
067         * @param text the date/time string to be parsed
068         * @return a <code>Calendar</code>, or <code>null</code> if the input could
069         *         not be parsed
070         * @throws IllegalArgumentException if a <code>null</code> argument is passed
071         */
072        public static Calendar parse(String text) {
073                if (text == null) {
074                        throw new IllegalArgumentException("argument can not be null");
075                }
076
077                // check optional leading sign
078                char sign;
079                int start;
080                if (text.startsWith("-")) {
081                        sign = '-';
082                        start = 1;
083                } else if (text.startsWith("+")) {
084                        sign = '+';
085                        start = 1;
086                } else {
087                        sign = '+'; // no sign specified, implied '+'
088                        start = 0;
089                }
090
091                /**
092                 * the expected format of the remainder of the string is:
093                 * YYYY-MM-DDThh:mm:ss.SSSTZD
094                 *
095                 * note that we cannot use java.text.SimpleDateFormat for
096                 * parsing because it can't handle years <= 0 and TZD's
097                 */
098
099                int year, month, day, hour, min, sec, ms;
100                String tzID;
101                try {
102                        // year (YYYY)
103                        year = Integer.parseInt(text.substring(start, start + 4));
104                        start += 4;
105                        // delimiter '-'
106                        if (text.charAt(start) != '-') {
107                                return null;
108                        }
109                        start++;
110                        // month (MM)
111                        month = Integer.parseInt(text.substring(start, start + 2));
112                        start += 2;
113                        // delimiter '-'
114                        if (text.charAt(start) != '-') {
115                                return null;
116                        }
117                        start++;
118                        // day (DD)
119                        day = Integer.parseInt(text.substring(start, start + 2));
120                        start += 2;
121                        // delimiter 'T'
122                        if (text.charAt(start) != 'T') {
123                                return null;
124                        }
125                        start++;
126                        // hour (hh)
127                        hour = Integer.parseInt(text.substring(start, start + 2));
128                        start += 2;
129                        // delimiter ':'
130                        if (text.charAt(start) != ':') {
131                                return null;
132                        }
133                        start++;
134                        // minute (mm)
135                        min = Integer.parseInt(text.substring(start, start + 2));
136                        start += 2;
137                        // delimiter ':'
138                        if (text.charAt(start) != ':') {
139                                return null;
140                        }
141                        start++;
142                        // second (ss)
143                        sec = Integer.parseInt(text.substring(start, start + 2));
144                        start += 2;
145                        // delimiter '.'
146                        if (text.charAt(start) != '.') {
147                                return null;
148                        }
149                        start++;
150                        // millisecond (SSS)
151                        ms = Integer.parseInt(text.substring(start, start + 3));
152                        start += 3;
153                        // time zone designator (Z or +00:00 or -00:00)
154                        if (text.charAt(start) == '+' || text.charAt(start) == '-') {
155                                // offset to UTC specified in the format +00:00/-00:00
156                                tzID = "GMT" + text.substring(start);
157                        } else if (text.substring(start).equals("Z")) {
158                                tzID = "GMT";
159                        } else {
160                                // invalid time zone designator
161                                return null;
162                        }
163                } catch (IndexOutOfBoundsException e) {
164                        return null;
165                } catch (NumberFormatException e) {
166                        return null;
167                }
168
169                TimeZone tz = TimeZone.getTimeZone(tzID);
170                // verify id of returned time zone (getTimeZone defaults to "GMT")
171                if (!tz.getID().equals(tzID)) {
172                        // invalid time zone
173                        return null;
174                }
175
176                // initialize Calendar object
177                Calendar cal = Calendar.getInstance(tz);
178                cal.setLenient(false);
179                // year and era
180                if (sign == '-' || year == 0) {
181                        // not CE, need to set era (BCE) and adjust year
182                        cal.set(Calendar.YEAR, year + 1);
183                        cal.set(Calendar.ERA, GregorianCalendar.BC);
184                } else {
185                        cal.set(Calendar.YEAR, year);
186                        cal.set(Calendar.ERA, GregorianCalendar.AD);
187                }
188                // month (0-based!)
189                cal.set(Calendar.MONTH, month - 1);
190                // day of month
191                cal.set(Calendar.DAY_OF_MONTH, day);
192                // hour
193                cal.set(Calendar.HOUR_OF_DAY, hour);
194                // minute
195                cal.set(Calendar.MINUTE, min);
196                // second
197                cal.set(Calendar.SECOND, sec);
198                // millisecond
199                cal.set(Calendar.MILLISECOND, ms);
200
201                try {
202                        /**
203                         * the following call will trigger an IllegalArgumentException
204                         * if any of the set values are illegal or out of range
205                         */
206                        cal.getTime();
207                        /**
208                         * in addition check the validity of the year
209                         */
210                        getYear(cal);
211                } catch (IllegalArgumentException e) {
212                        return null;
213                }
214
215                return cal;
216        }
217
218        /**
219         * Formats a <code>Calendar</code> value into an ISO8601-compliant
220         * date/time string.
221         *
222         * @param cal the time value to be formatted into a date/time string.
223         * @return the formatted date/time string.
224         * @throws IllegalArgumentException if a <code>null</code> argument is passed
225         * or the calendar cannot be represented as defined by ISO 8601 (i.e. year
226         * with more than four digits).
227         */
228        public static String format(Calendar cal) throws IllegalArgumentException {
229                if (cal == null) {
230                        throw new IllegalArgumentException("argument can not be null");
231                }
232
233                /**
234                 * the format of the date/time string is:
235                 * YYYY-MM-DDThh:mm:ss.SSSTZD
236                 *
237                 * note that we cannot use java.text.SimpleDateFormat for
238                 * formatting because it can't handle years <= 0 and TZD's
239                 */
240                StringBuilder buf = new StringBuilder();
241                // year ([-]YYYY)
242                appendZeroPaddedInt(buf, getYear(cal), 4);
243                buf.append('-');
244                // month (MM)
245                appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2);
246                buf.append('-');
247                // day (DD)
248                appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2);
249                buf.append('T');
250                // hour (hh)
251                appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2);
252                buf.append(':');
253                // minute (mm)
254                appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2);
255                buf.append(':');
256                // second (ss)
257                appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2);
258                buf.append('.');
259                // millisecond (SSS)
260                appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3);
261                // time zone designator (Z or +00:00 or -00:00)
262                TimeZone tz = cal.getTimeZone();
263                // determine offset of timezone from UTC (incl. daylight saving)
264                int offset = tz.getOffset(cal.getTimeInMillis());
265                if (offset != 0) {
266                        int hours = Math.abs((offset / (60 * 1000)) / 60);
267                        int minutes = Math.abs((offset / (60 * 1000)) % 60);
268                        buf.append(offset < 0 ? '-' : '+');
269                        appendZeroPaddedInt(buf, hours, 2);
270                        buf.append(':');
271                        appendZeroPaddedInt(buf, minutes, 2);
272                } else {
273                        buf.append('Z');
274                }
275                return buf.toString();
276        }
277
278        /**
279         * Returns the astronomical year of the given calendar.
280         *
281         * @param cal a calendar instance.
282         * @return the astronomical year.
283         * @throws IllegalArgumentException if calendar cannot be represented as
284         *                                  defined by ISO 8601 (i.e. year with more
285         *                                  than four digits).
286         */
287        public static int getYear(Calendar cal) throws IllegalArgumentException {
288                // determine era and adjust year if necessary
289                int year = cal.get(Calendar.YEAR);
290                if (cal.isSet(Calendar.ERA)
291                                && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
292                        /**
293                         * calculate year using astronomical system:
294                         * year n BCE => astronomical year -n + 1
295                         */
296                        year = 0 - year + 1;
297                }
298
299                if (year > 9999 || year < -9999) {
300                        throw new IllegalArgumentException("Calendar has more than four " +
301                                        "year digits, cannot be formatted as ISO8601: " + year);
302                }
303                return year;
304        }
305
306        /**
307         * Appends a zero-padded number to the given string buffer.
308         * <p/>
309         * This is an internal helper method which doesn't perform any
310         * validation on the given arguments.
311         *
312         * @param buf String buffer to append to
313         * @param n number to append
314         * @param precision number of digits to append
315         */
316        private static void appendZeroPaddedInt(StringBuilder buf, int n, int precision) {
317                if (n < 0) {
318                        buf.append('-');
319                        n = -n;
320                }
321
322                for (int exp = precision - 1; exp > 0; exp--) {
323                        if (n < Math.pow(10, exp)) {
324                                buf.append('0');
325                        } else {
326                                break;
327                        }
328                }
329                buf.append(n);
330        }
331}