001/*
002 * (C) Copyright 2006-2011 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 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.ecm.automation.client.model;
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 * 
030 * @author "Stephane Lacoin [aka matic] <slacoin at nuxeo.com>"
031 */
032public class DateParser {
033
034    public static Calendar parse(String str) throws ParseException {
035        if (str == null) {
036            return null;
037        }
038        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
039        cal.clear();
040        int len = str.length();
041        if (len == 0) { // empty string
042            // TODO throw error?
043            return cal;
044        }
045        int i = 0;
046        i = readYear(cal, str, i);
047        i = readCharOpt('-', cal, str, i);
048        if (i == -1) {
049            return cal;
050        }
051        i = readMonth(cal, str, i);
052        i = readCharOpt('-', cal, str, i);
053        if (i == -1) {
054            return cal;
055        }
056        i = readDay(cal, str, i);
057        i = readCharOpt('T', cal, str, i);
058        if (i == -1) {
059            return cal;
060        }
061        i = readHours(cal, str, i);
062        i = readCharOpt(':', cal, str, i);
063        if (i == -1) {
064            return cal;
065        }
066        i = readMinutes(cal, str, i);
067        if (isChar(':', str, i)) {
068            i = readSeconds(cal, str, i + 1);
069            if (isChar('.', str, i)) {
070                i = readMilliseconds(cal, str, i + 1);
071            }
072        }
073        if (i > -1) {
074            readTimeZone(cal, str, i);
075        }
076        return cal;
077    }
078
079    public static Date parseW3CDateTime(String str) {
080        if (str == null) {
081            return null;
082        }
083        try {
084            return parse(str).getTime();
085        } catch (ParseException e) {
086            throw new IllegalArgumentException("Failed to parse ISO 8601 date: " + str, e);
087        }
088    }
089
090    /**
091     * 2011-10-23T12:00:00.00Z
092     * 
093     * @param date
094     * @return
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        StringBuilder buf = new StringBuilder(32);
103        return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append(
104                pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append(
105                pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('Z').toString();
106    }
107
108    public static String formatW3CDateTimeMs(Date date) {
109        if (date == null) {
110            return null;
111        }
112        Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
113        cal.setTime(date);
114        StringBuilder buf = new StringBuilder(32);
115        return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append(
116                pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append(
117                pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('.').append(
118                pad3(cal.get(Calendar.MILLISECOND))).append('Z').toString();
119    }
120
121    private final static String pad(int i) {
122        return i < 10 ? "0".concat(String.valueOf(i)) : String.valueOf(i);
123    }
124
125    private final static String pad3(int i) {
126        if (i < 10) {
127            return "00".concat(String.valueOf(i));
128        } else if (i < 100) {
129            return "0".concat(String.valueOf(i));
130        } else {
131            return String.valueOf(i);
132        }
133    }
134
135    private final static int readYear(Calendar cal, String str, int off) throws ParseException {
136        if (str.length() >= off + 4) {
137            cal.set(Calendar.YEAR, Integer.parseInt(str.substring(off, off + 4)));
138            return off + 4;
139        }
140        throw new ParseException("Invalid year in date '" + str + "'", off);
141    }
142
143    private final static int readMonth(Calendar cal, String str, int off) throws ParseException {
144        if (str.length() >= off + 2) {
145            cal.set(Calendar.MONTH, Integer.parseInt(str.substring(off, off + 2)) - 1);
146            return off + 2;
147        }
148        throw new ParseException("Invalid month in date '" + str + "'", off);
149    }
150
151    private final static int readDay(Calendar cal, String str, int off) throws ParseException {
152        if (str.length() >= off + 2) {
153            cal.set(Calendar.DATE, Integer.parseInt(str.substring(off, off + 2)));
154            return off + 2;
155        }
156        throw new ParseException("Invalid day in date '" + str + "'", off);
157    }
158
159    private final static int readHours(Calendar cal, String str, int off) throws ParseException {
160        if (str.length() >= off + 2) {
161            cal.set(Calendar.HOUR, Integer.parseInt(str.substring(off, off + 2)));
162            return off + 2;
163        }
164        throw new ParseException("Invalid hours in date '" + str + "'", off);
165    }
166
167    private final static int readMinutes(Calendar cal, String str, int off) throws ParseException {
168        if (str.length() >= off + 2) {
169            cal.set(Calendar.MINUTE, Integer.parseInt(str.substring(off, off + 2)));
170            return off + 2;
171        }
172        throw new ParseException("Invalid minutes in date '" + str + "'", off);
173    }
174
175    private final static int readSeconds(Calendar cal, String str, int off) throws ParseException {
176        if (str.length() >= off + 2) {
177            cal.set(Calendar.SECOND, Integer.parseInt(str.substring(off, off + 2)));
178            return off + 2;
179        }
180        throw new ParseException("Invalid seconds in date '" + str + "'", off);
181    }
182
183    /**
184     * Return -1 if no more content to read or the offset of the expected TZ
185     * 
186     * @param cal
187     * @param str
188     * @param off
189     * @return
190     * @throws ParseException
191     */
192    private final static int readMilliseconds(Calendar cal, String str, int off) throws ParseException {
193        int e = str.indexOf('Z', off);
194        if (e == -1) {
195            e = str.indexOf('+', off);
196            if (e == -1) {
197                e = str.indexOf('-', off);
198            }
199        }
200        String ms = e == -1 ? str.substring(off) : str.substring(off, e);
201        // need to normalize the ms fraction to 3 digits.
202        // If less than 3 digits right pad with 0
203        // If more than 3 digits truncate to 3 digits.
204        int mslen = ms.length();
205        if (mslen > 0) {
206            int f = 0;
207            switch (mslen) {
208            case 1:
209                f = Integer.parseInt(ms) * 100;
210                break;
211            case 2:
212                f = Integer.parseInt(ms) * 10;
213                break;
214            case 3:
215                f = Integer.parseInt(ms);
216                break;
217            default: // truncate
218                f = Integer.parseInt(ms.substring(0, 3));
219                break;
220            }
221            cal.set(Calendar.MILLISECOND, f);
222        }
223        return e;
224    }
225
226    private static final boolean isChar(char c, String str, int off) {
227        return str.length() > off && str.charAt(off) == c;
228    }
229
230    private static final int readCharOpt(char c, Calendar cal, String str, int off) {
231        if (str.length() > off) {
232            if (str.charAt(off) == c) {
233                return off + 1;
234            }
235        }
236        return -1;
237    }
238
239    private final static boolean readTimeZone(Calendar cal, String str, int off) throws ParseException {
240        int len = str.length();
241        if (len == off) {
242            return false;
243        }
244        char c = str.charAt(off);
245        if (c == 'Z') {
246            return true;
247        }
248        off++;
249        boolean plus = false;
250        if (c == '+') {
251            plus = true;
252        } else if (c != '-') {
253            throw new ParseException("Only Z, +, - prefixes are allowed in TZ", off);
254        }
255        int h = 0;
256        int m = 0;
257        int d = len - off;
258        if (d == 2) {
259            h = Integer.parseInt(str.substring(off, off + 2));
260        } else if (d == 5) {
261            h = Integer.parseInt(str.substring(off, off + 2));
262            m = Integer.parseInt(str.substring(off + 3, off + 5));
263            // we do not check for ':'. we assume it is in the correct format
264        } else {
265            throw new ParseException("Invalid TZ in \"" + str + "\"", off);
266        }
267
268        if (plus) {
269            cal.add(Calendar.HOUR, -h);
270            cal.add(Calendar.MINUTE, -m);
271        } else {
272            cal.add(Calendar.HOUR, h);
273            cal.add(Calendar.MINUTE, m);
274        }
275
276        return true;
277    }
278
279}