/* * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation */ package org.nuxeo.ecm.automation.client.model; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; /** * Parse / format ISO 8601 dates. * * @author "Stephane Lacoin [aka matic] <slacoin at nuxeo.com>" */ public class DateParser { public static Calendar parse(String str) throws ParseException { if (str == null) { return null; } Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); cal.clear(); int len = str.length(); if (len == 0) { // empty string // TODO throw error? return cal; } int i = 0; i = readYear(cal, str, i); i = readCharOpt('-', cal, str, i); if (i == -1) { return cal; } i = readMonth(cal, str, i); i = readCharOpt('-', cal, str, i); if (i == -1) { return cal; } i = readDay(cal, str, i); i = readCharOpt('T', cal, str, i); if (i == -1) { return cal; } i = readHours(cal, str, i); i = readCharOpt(':', cal, str, i); if (i == -1) { return cal; } i = readMinutes(cal, str, i); if (isChar(':', str, i)) { i = readSeconds(cal, str, i + 1); if (isChar('.', str, i)) { i = readMilliseconds(cal, str, i + 1); } } if (i > -1) { readTimeZone(cal, str, i); } return cal; } public static Date parseW3CDateTime(String str) { if (str == null) { return null; } try { return parse(str).getTime(); } catch (ParseException e) { throw new IllegalArgumentException("Failed to parse ISO 8601 date: " + str, e); } } /** * 2011-10-23T12:00:00.00Z * * @param date * @return */ public static String formatW3CDateTime(Date date) { if (date == null) { return null; } Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); cal.setTime(date); StringBuilder buf = new StringBuilder(32); return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append( pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append( pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('Z').toString(); } public static String formatW3CDateTimeMs(Date date) { if (date == null) { return null; } Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); cal.setTime(date); StringBuilder buf = new StringBuilder(32); return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append( pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append( pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('.').append( pad3(cal.get(Calendar.MILLISECOND))).append('Z').toString(); } private final static String pad(int i) { return i < 10 ? "0".concat(String.valueOf(i)) : String.valueOf(i); } private final static String pad3(int i) { if (i < 10) { return "00".concat(String.valueOf(i)); } else if (i < 100) { return "0".concat(String.valueOf(i)); } else { return String.valueOf(i); } } private final static int readYear(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 4) { cal.set(Calendar.YEAR, Integer.parseInt(str.substring(off, off + 4))); return off + 4; } throw new ParseException("Invalid year in date '" + str + "'", off); } private final static int readMonth(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 2) { cal.set(Calendar.MONTH, Integer.parseInt(str.substring(off, off + 2)) - 1); return off + 2; } throw new ParseException("Invalid month in date '" + str + "'", off); } private final static int readDay(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 2) { cal.set(Calendar.DATE, Integer.parseInt(str.substring(off, off + 2))); return off + 2; } throw new ParseException("Invalid day in date '" + str + "'", off); } private final static int readHours(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 2) { cal.set(Calendar.HOUR, Integer.parseInt(str.substring(off, off + 2))); return off + 2; } throw new ParseException("Invalid hours in date '" + str + "'", off); } private final static int readMinutes(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 2) { cal.set(Calendar.MINUTE, Integer.parseInt(str.substring(off, off + 2))); return off + 2; } throw new ParseException("Invalid minutes in date '" + str + "'", off); } private final static int readSeconds(Calendar cal, String str, int off) throws ParseException { if (str.length() >= off + 2) { cal.set(Calendar.SECOND, Integer.parseInt(str.substring(off, off + 2))); return off + 2; } throw new ParseException("Invalid seconds in date '" + str + "'", off); } /** * Return -1 if no more content to read or the offset of the expected TZ * * @param cal * @param str * @param off * @return * @throws ParseException */ private final static int readMilliseconds(Calendar cal, String str, int off) throws ParseException { int e = str.indexOf('Z', off); if (e == -1) { e = str.indexOf('+', off); if (e == -1) { e = str.indexOf('-', off); } } String ms = e == -1 ? str.substring(off) : str.substring(off, e); // need to normalize the ms fraction to 3 digits. // If less than 3 digits right pad with 0 // If more than 3 digits truncate to 3 digits. int mslen = ms.length(); if (mslen > 0) { int f = 0; switch (mslen) { case 1: f = Integer.parseInt(ms) * 100; break; case 2: f = Integer.parseInt(ms) * 10; break; case 3: f = Integer.parseInt(ms); break; default: // truncate f = Integer.parseInt(ms.substring(0, 3)); break; } cal.set(Calendar.MILLISECOND, f); } return e; } private static final boolean isChar(char c, String str, int off) { return str.length() > off && str.charAt(off) == c; } private static final int readCharOpt(char c, Calendar cal, String str, int off) { if (str.length() > off) { if (str.charAt(off) == c) { return off + 1; } } return -1; } private final static boolean readTimeZone(Calendar cal, String str, int off) throws ParseException { int len = str.length(); if (len == off) { return false; } char c = str.charAt(off); if (c == 'Z') { return true; } off++; boolean plus = false; if (c == '+') { plus = true; } else if (c != '-') { throw new ParseException("Only Z, +, - prefixes are allowed in TZ", off); } int h = 0; int m = 0; int d = len - off; if (d == 2) { h = Integer.parseInt(str.substring(off, off + 2)); } else if (d == 5) { h = Integer.parseInt(str.substring(off, off + 2)); m = Integer.parseInt(str.substring(off + 3, off + 5)); // we do not check for ':'. we assume it is in the correct format } else { throw new ParseException("Invalid TZ in \"" + str + "\"", off); } if (plus) { cal.add(Calendar.HOUR, -h); cal.add(Calendar.MINUTE, -m); } else { cal.add(Calendar.HOUR, h); cal.add(Calendar.MINUTE, m); } return true; } }