001/*
002 * Copyright (c) 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Nuxeo - initial API and implementation
011 */
012package org.nuxeo.ecm.core.schema.utils;
013
014import java.text.ParseException;
015import java.util.Calendar;
016import java.util.Date;
017import java.util.GregorianCalendar;
018import java.util.TimeZone;
019
020/**
021 * Parse / format ISO 8601 dates.
022 *
023 * @author "Stephane Lacoin [aka matic] <slacoin at nuxeo.com>"
024 */
025public class DateParser {
026
027    public static Calendar parse(String str) throws ParseException {
028        if (str == null) {
029            return null;
030        }
031        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
032        cal.clear();
033        int len = str.length();
034        if (len == 0) { // empty string
035            // TODO throw error?
036            return cal;
037        }
038        int i = 0;
039        i = readYear(cal, str, i);
040        i = readCharOpt('-', cal, str, i);
041        if (i == -1) {
042            return cal;
043        }
044        i = readMonth(cal, str, i);
045        i = readCharOpt('-', cal, str, i);
046        if (i == -1) {
047            return cal;
048        }
049        i = readDay(cal, str, i);
050        i = readCharOpt('T', cal, str, i);
051        if (i == -1) {
052            return cal;
053        }
054        i = readHours(cal, str, i);
055        i = readCharOpt(':', cal, str, i);
056        if (i == -1) {
057            return cal;
058        }
059        i = readMinutes(cal, str, i);
060        if (isChar(':', str, i)) {
061            i = readSeconds(cal, str, i + 1);
062            if (isChar('.', str, i)) {
063                i = readMilliseconds(cal, str, i + 1);
064            }
065        }
066        if (i > -1) {
067            readTimeZone(cal, str, i);
068        }
069        return cal;
070    }
071
072    public static Date parseW3CDateTime(String str) {
073        if (str == null || "".equals(str)) {
074            return null;
075        }
076        try {
077            return parse(str).getTime();
078        } catch (ParseException e) {
079            throw new IllegalArgumentException("Failed to parse ISO 8601 date: " + str, e);
080        }
081    }
082
083    /**
084     * 2011-10-23T12:00:00.00Z
085     *
086     * @param date
087     * @return
088     */
089    public static String formatW3CDateTime(Date date) {
090        if (date == null) {
091            return null;
092        }
093        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
094        cal.setTime(date);
095        StringBuilder buf = new StringBuilder(32);
096        return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append(
097                pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append(
098                pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('.').append(
099                pad(cal.get(Calendar.MILLISECOND) / 10)).append('Z').toString();
100    }
101
102    /**
103     * 2011-10-23T12:00:00.00Z
104     *
105     * @param calendar
106     * @return
107     *
108     * @since 7.3
109     */
110    public static String formatW3CDateTime(Calendar calendar) {
111        if (calendar == null) {
112            return null;
113        }
114        return formatW3CDateTime(calendar.getTime());
115    }
116
117    private final static String pad(int i) {
118        return i < 10 ? "0".concat(String.valueOf(i)) : String.valueOf(i);
119    }
120
121    private final 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 final 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 final 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 final 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 final 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 final 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     * @param cal
173     * @param str
174     * @param off
175     * @return
176     * @throws ParseException
177     */
178    private final static int readMilliseconds(Calendar cal, String str, int off) throws ParseException {
179        int e = str.indexOf('Z', off);
180        if (e == -1) {
181            e = str.indexOf('+', off);
182            if (e == -1) {
183                e = str.indexOf('-', off);
184            }
185        }
186        String ms = e == -1 ? str.substring(off) : str.substring(off, e);
187        // need to normalize the ms fraction to 3 digits.
188        // If less than 3 digits right pad with 0
189        // If more than 3 digits truncate to 3 digits.
190        int mslen = ms.length();
191        if (mslen > 0) {
192            int f = 0;
193            switch (mslen) {
194            case 1:
195                f = Integer.parseInt(ms) * 100;
196                break;
197            case 2:
198                f = Integer.parseInt(ms) * 10;
199                break;
200            case 3:
201                f = Integer.parseInt(ms);
202                break;
203            default: // truncate
204                f = Integer.parseInt(ms.substring(0, 3));
205                break;
206            }
207            cal.set(Calendar.MILLISECOND, f);
208        }
209        return e;
210    }
211
212    private static final boolean isChar(char c, String str, int off) {
213        return str.length() > off && str.charAt(off) == c;
214    }
215
216    private static final int readCharOpt(char c, Calendar cal, String str, int off) {
217        if (str.length() > off) {
218            if (str.charAt(off) == c) {
219                return off + 1;
220            }
221        }
222        return -1;
223    }
224
225    private final static boolean readTimeZone(Calendar cal, String str, int off) throws ParseException {
226        int len = str.length();
227        if (len == off) {
228            return false;
229        }
230        char c = str.charAt(off);
231        if (c == 'Z') {
232            return true;
233        }
234        off++;
235        boolean plus = false;
236        if (c == '+') {
237            plus = true;
238        } else if (c != '-') {
239            throw new ParseException("Only Z, +, - prefixes are allowed in TZ", off);
240        }
241        int h = 0;
242        int m = 0;
243        int d = len - off;
244        /**
245         * We check here the different format of timezone * +02 (d=2 : doesn't seem to be in ISO-8601 but left for
246         * compat) * +02:00 (d=5) * +0200 (d=4)
247         */
248        if (d == 2) {
249            h = Integer.parseInt(str.substring(off, off + 2));
250        } else if (d == 5) {
251            h = Integer.parseInt(str.substring(off, off + 2));
252            m = Integer.parseInt(str.substring(off + 3, off + 5));
253            // we do not check for ':'. we assume it is in the correct format
254        } else if (d == 4) {
255            h = Integer.parseInt(str.substring(off, off + 2));
256            m = Integer.parseInt(str.substring(off + 2, off + 4));
257        } else {
258            throw new ParseException("Invalid TZ in \"" + str + "\"", off);
259        }
260
261        if (plus) {
262            cal.add(Calendar.HOUR, -h);
263            cal.add(Calendar.MINUTE, -m);
264        } else {
265            cal.add(Calendar.HOUR, h);
266            cal.add(Calendar.MINUTE, m);
267        }
268
269        return true;
270    }
271
272}