001/* 002 * Copyright (c) 2006-2012 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Nuxeo - initial API and implementation 011 */ 012package org.nuxeo.ecm.core.schema.utils; 013 014import java.text.ParseException; 015import java.util.Calendar; 016import java.util.Date; 017import java.util.GregorianCalendar; 018import java.util.TimeZone; 019 020/** 021 * Parse / format ISO 8601 dates. 022 * 023 * @author "Stephane Lacoin [aka matic] <slacoin at nuxeo.com>" 024 */ 025public class DateParser { 026 027 public static Calendar parse(String str) throws ParseException { 028 if (str == null) { 029 return null; 030 } 031 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 032 cal.clear(); 033 int len = str.length(); 034 if (len == 0) { // empty string 035 // TODO throw error? 036 return cal; 037 } 038 int i = 0; 039 i = readYear(cal, str, i); 040 i = readCharOpt('-', cal, str, i); 041 if (i == -1) { 042 return cal; 043 } 044 i = readMonth(cal, str, i); 045 i = readCharOpt('-', cal, str, i); 046 if (i == -1) { 047 return cal; 048 } 049 i = readDay(cal, str, i); 050 i = readCharOpt('T', cal, str, i); 051 if (i == -1) { 052 return cal; 053 } 054 i = readHours(cal, str, i); 055 i = readCharOpt(':', cal, str, i); 056 if (i == -1) { 057 return cal; 058 } 059 i = readMinutes(cal, str, i); 060 if (isChar(':', str, i)) { 061 i = readSeconds(cal, str, i + 1); 062 if (isChar('.', str, i)) { 063 i = readMilliseconds(cal, str, i + 1); 064 } 065 } 066 if (i > -1) { 067 readTimeZone(cal, str, i); 068 } 069 return cal; 070 } 071 072 public static Date parseW3CDateTime(String str) { 073 if (str == null || "".equals(str)) { 074 return null; 075 } 076 try { 077 return parse(str).getTime(); 078 } catch (ParseException e) { 079 throw new IllegalArgumentException("Failed to parse ISO 8601 date: " + str, e); 080 } 081 } 082 083 /** 084 * 2011-10-23T12:00:00.00Z 085 * 086 * @param date 087 * @return 088 */ 089 public static String formatW3CDateTime(Date date) { 090 if (date == null) { 091 return null; 092 } 093 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 094 cal.setTime(date); 095 StringBuilder buf = new StringBuilder(32); 096 return buf.append(cal.get(Calendar.YEAR)).append('-').append(pad(cal.get(Calendar.MONTH) + 1)).append('-').append( 097 pad(cal.get(Calendar.DATE))).append('T').append(pad(cal.get(Calendar.HOUR_OF_DAY))).append(':').append( 098 pad(cal.get(Calendar.MINUTE))).append(':').append(pad(cal.get(Calendar.SECOND))).append('.').append( 099 pad(cal.get(Calendar.MILLISECOND) / 10)).append('Z').toString(); 100 } 101 102 /** 103 * 2011-10-23T12:00:00.00Z 104 * 105 * @param calendar 106 * @return 107 * 108 * @since 7.3 109 */ 110 public static String formatW3CDateTime(Calendar calendar) { 111 if (calendar == null) { 112 return null; 113 } 114 return formatW3CDateTime(calendar.getTime()); 115 } 116 117 private final static String pad(int i) { 118 return i < 10 ? "0".concat(String.valueOf(i)) : String.valueOf(i); 119 } 120 121 private final 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 final 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 final 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 final 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 final 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 final 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 * @param cal 173 * @param str 174 * @param off 175 * @return 176 * @throws ParseException 177 */ 178 private final static int readMilliseconds(Calendar cal, String str, int off) throws ParseException { 179 int e = str.indexOf('Z', off); 180 if (e == -1) { 181 e = str.indexOf('+', off); 182 if (e == -1) { 183 e = str.indexOf('-', off); 184 } 185 } 186 String ms = e == -1 ? str.substring(off) : str.substring(off, e); 187 // need to normalize the ms fraction to 3 digits. 188 // If less than 3 digits right pad with 0 189 // If more than 3 digits truncate to 3 digits. 190 int mslen = ms.length(); 191 if (mslen > 0) { 192 int f = 0; 193 switch (mslen) { 194 case 1: 195 f = Integer.parseInt(ms) * 100; 196 break; 197 case 2: 198 f = Integer.parseInt(ms) * 10; 199 break; 200 case 3: 201 f = Integer.parseInt(ms); 202 break; 203 default: // truncate 204 f = Integer.parseInt(ms.substring(0, 3)); 205 break; 206 } 207 cal.set(Calendar.MILLISECOND, f); 208 } 209 return e; 210 } 211 212 private static final boolean isChar(char c, String str, int off) { 213 return str.length() > off && str.charAt(off) == c; 214 } 215 216 private static final int readCharOpt(char c, Calendar cal, String str, int off) { 217 if (str.length() > off) { 218 if (str.charAt(off) == c) { 219 return off + 1; 220 } 221 } 222 return -1; 223 } 224 225 private final static boolean readTimeZone(Calendar cal, String str, int off) throws ParseException { 226 int len = str.length(); 227 if (len == off) { 228 return false; 229 } 230 char c = str.charAt(off); 231 if (c == 'Z') { 232 return true; 233 } 234 off++; 235 boolean plus = false; 236 if (c == '+') { 237 plus = true; 238 } else if (c != '-') { 239 throw new ParseException("Only Z, +, - prefixes are allowed in TZ", off); 240 } 241 int h = 0; 242 int m = 0; 243 int d = len - off; 244 /** 245 * We check here the different format of timezone * +02 (d=2 : doesn't seem to be in ISO-8601 but left for 246 * compat) * +02:00 (d=5) * +0200 (d=4) 247 */ 248 if (d == 2) { 249 h = Integer.parseInt(str.substring(off, off + 2)); 250 } else if (d == 5) { 251 h = Integer.parseInt(str.substring(off, off + 2)); 252 m = Integer.parseInt(str.substring(off + 3, off + 5)); 253 // we do not check for ':'. we assume it is in the correct format 254 } else if (d == 4) { 255 h = Integer.parseInt(str.substring(off, off + 2)); 256 m = Integer.parseInt(str.substring(off + 2, off + 4)); 257 } else { 258 throw new ParseException("Invalid TZ in \"" + str + "\"", off); 259 } 260 261 if (plus) { 262 cal.add(Calendar.HOUR, -h); 263 cal.add(Calendar.MINUTE, -m); 264 } else { 265 cal.add(Calendar.HOUR, h); 266 cal.add(Calendar.MINUTE, m); 267 } 268 269 return true; 270 } 271 272}