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