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}