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}