001/*
002 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     "Stephane Lacoin [aka matic] <slacoin at nuxeo.com>"
018 */
019package org.nuxeo.ecm.core.schema.utils;
020
021import java.text.ParseException;
022import java.util.Calendar;
023import java.util.Date;
024import java.util.GregorianCalendar;
025import java.util.TimeZone;
026
027/**
028 * Parse / format ISO 8601 dates.
029 */
030public class DateParser {
031
032    /**
033     * @since 8.2
034     */
035    public static final String W3C_DATE_FORMAT = "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ";
036
037    public static Calendar parse(String str) throws ParseException {
038        if (str == null) {
039            return null;
040        }
041        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
042        cal.clear();
043        int len = str.length();
044        if (len == 0) { // empty string
045            // TODO throw error?
046            return cal;
047        }
048        int i = 0;
049        i = readYear(cal, str, i);
050        i = readCharOpt('-', cal, str, i);
051        if (i == -1) {
052            return cal;
053        }
054        i = readMonth(cal, str, i);
055        i = readCharOpt('-', cal, str, i);
056        if (i == -1) {
057            return cal;
058        }
059        i = readDay(cal, str, i);
060        i = readCharOpt('T', cal, str, i);
061        if (i == -1) {
062            return cal;
063        }
064        i = readHours(cal, str, i);
065        i = readCharOpt(':', cal, str, i);
066        if (i == -1) {
067            return cal;
068        }
069        i = readMinutes(cal, str, i);
070        if (isChar(':', str, i)) {
071            i = readSeconds(cal, str, i + 1);
072            if (isChar('.', str, i)) {
073                i = readMilliseconds(cal, str, i + 1);
074            }
075        }
076        if (i > -1) {
077            readTimeZone(cal, str, i);
078        }
079        return cal;
080    }
081
082    public static Date parseW3CDateTime(String str) {
083        if (str == null || "".equals(str)) {
084            return null;
085        }
086        try {
087            return parse(str).getTime();
088        } catch (ParseException e) {
089            throw new IllegalArgumentException("Failed to parse ISO 8601 date: " + str, e);
090        }
091    }
092
093    /**
094     * 2011-10-23T12:00:00.000Z
095     */
096    public static String formatW3CDateTime(Date date) {
097        if (date == null) {
098            return null;
099        }
100        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
101        cal.setTime(date);
102        return formatW3CDateTime(cal);
103    }
104
105    /**
106     * 2011-10-23T12:00:00.000Z
107     *
108     * @since 7.3
109     */
110    public static String formatW3CDateTime(Calendar calendar) {
111        if (calendar == null) {
112            return null;
113        }
114        return String.format(W3C_DATE_FORMAT, calendar.get(Calendar.YEAR),
115                calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DATE),
116                calendar.get(Calendar.HOUR_OF_DAY),
117                calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND),
118                calendar.get(Calendar.MILLISECOND));
119    }
120
121    private static int readYear(Calendar cal, String str, int off) throws ParseException {
122        if (str.length() >= off + 4) {
123            cal.set(Calendar.YEAR, Integer.parseInt(str.substring(off, off + 4)));
124            return off + 4;
125        }
126        throw new ParseException("Invalid year in date '" + str + "'", off);
127    }
128
129    private static int readMonth(Calendar cal, String str, int off) throws ParseException {
130        if (str.length() >= off + 2) {
131            cal.set(Calendar.MONTH, Integer.parseInt(str.substring(off, off + 2)) - 1);
132            return off + 2;
133        }
134        throw new ParseException("Invalid month in date '" + str + "'", off);
135    }
136
137    private static int readDay(Calendar cal, String str, int off) throws ParseException {
138        if (str.length() >= off + 2) {
139            cal.set(Calendar.DATE, Integer.parseInt(str.substring(off, off + 2)));
140            return off + 2;
141        }
142        throw new ParseException("Invalid day in date '" + str + "'", off);
143    }
144
145    private static int readHours(Calendar cal, String str, int off) throws ParseException {
146        if (str.length() >= off + 2) {
147            cal.set(Calendar.HOUR, Integer.parseInt(str.substring(off, off + 2)));
148            return off + 2;
149        }
150        throw new ParseException("Invalid hours in date '" + str + "'", off);
151    }
152
153    private static int readMinutes(Calendar cal, String str, int off) throws ParseException {
154        if (str.length() >= off + 2) {
155            cal.set(Calendar.MINUTE, Integer.parseInt(str.substring(off, off + 2)));
156            return off + 2;
157        }
158        throw new ParseException("Invalid minutes in date '" + str + "'", off);
159    }
160
161    private static int readSeconds(Calendar cal, String str, int off) throws ParseException {
162        if (str.length() >= off + 2) {
163            cal.set(Calendar.SECOND, Integer.parseInt(str.substring(off, off + 2)));
164            return off + 2;
165        }
166        throw new ParseException("Invalid seconds in date '" + str + "'", off);
167    }
168
169    /**
170     * Return -1 if no more content to read or the offset of the expected TZ
171     */
172    private static int readMilliseconds(Calendar cal, String str, int off) throws ParseException {
173        int e = str.indexOf('Z', off);
174        if (e == -1) {
175            e = str.indexOf('+', off);
176            if (e == -1) {
177                e = str.indexOf('-', off);
178            }
179        }
180        String ms = e == -1 ? str.substring(off) : str.substring(off, e);
181        // need to normalize the ms fraction to 3 digits.
182        // If less than 3 digits right pad with 0
183        // If more than 3 digits truncate to 3 digits.
184        int mslen = ms.length();
185        if (mslen > 0) {
186            int f = 0;
187            switch (mslen) {
188            case 1:
189                f = Integer.parseInt(ms) * 100;
190                break;
191            case 2:
192                f = Integer.parseInt(ms) * 10;
193                break;
194            case 3:
195                f = Integer.parseInt(ms);
196                break;
197            default: // truncate
198                f = Integer.parseInt(ms.substring(0, 3));
199                break;
200            }
201            cal.set(Calendar.MILLISECOND, f);
202        }
203        return e;
204    }
205
206    private static boolean isChar(char c, String str, int off) {
207        return str.length() > off && str.charAt(off) == c;
208    }
209
210    private static int readCharOpt(char c, Calendar cal, String str, int off) {
211        if (str.length() > off) {
212            if (str.charAt(off) == c) {
213                return off + 1;
214            }
215        }
216        return -1;
217    }
218
219    private static boolean readTimeZone(Calendar cal, String str, int off) throws ParseException {
220        int len = str.length();
221        if (len == off) {
222            return false;
223        }
224        char c = str.charAt(off);
225        if (c == 'Z') {
226            return true;
227        }
228        off++;
229        boolean plus = false;
230        if (c == '+') {
231            plus = true;
232        } else if (c != '-') {
233            throw new ParseException("Only Z, +, - prefixes are allowed in TZ", off);
234        }
235        int h = 0;
236        int m = 0;
237        int d = len - off;
238        /**
239         * We check here the different format of timezone * +02 (d=2 : doesn't seem to be in ISO-8601 but left for
240         * compat) * +02:00 (d=5) * +0200 (d=4)
241         */
242        if (d == 2) {
243            h = Integer.parseInt(str.substring(off, off + 2));
244        } else if (d == 5) {
245            h = Integer.parseInt(str.substring(off, off + 2));
246            m = Integer.parseInt(str.substring(off + 3, off + 5));
247            // we do not check for ':'. we assume it is in the correct format
248        } else if (d == 4) {
249            h = Integer.parseInt(str.substring(off, off + 2));
250            m = Integer.parseInt(str.substring(off + 2, off + 4));
251        } else {
252            throw new ParseException("Invalid TZ in \"" + str + "\"", off);
253        }
254
255        if (plus) {
256            cal.add(Calendar.HOUR, -h);
257            cal.add(Calendar.MINUTE, -m);
258        } else {
259            cal.add(Calendar.HOUR, h);
260            cal.add(Calendar.MINUTE, m);
261        }
262
263        return true;
264    }
265
266}