001package co.codewizards.cloudstore.core.util; 002 003import java.util.Calendar; 004import java.util.Date; 005import java.util.GregorianCalendar; 006import java.util.Locale; 007import java.util.TimeZone; 008 009/** 010 * The <code>ISO8601</code> utility class provides helper methods 011 * to deal with date/time formatting using a specific ISO8601-compliant 012 * format (see <a href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>). 013 * <p/> 014 * The currently supported format is: 015 * <pre> 016 * ±YYYY-MM-DDThh:mm:ss.SSSTZD 017 * </pre> 018 * where: 019 * <pre> 020 * ±YYYY = four-digit year with optional sign where values <= 0 are 021 * denoting years BCE and values > 0 are denoting years CE, 022 * e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE, 023 * 0001 denotes the year 1 CE, and so on... 024 * MM = two-digit month (01=January, etc.) 025 * DD = two-digit day of month (01 through 31) 026 * hh = two digits of hour (00 through 23) (am/pm NOT allowed) 027 * mm = two digits of minute (00 through 59) 028 * ss = two digits of second (00 through 59) 029 * SSS = three digits of milliseconds (000 through 999) 030 * TZD = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC 031 * in the form of +hh:mm or -hh:mm 032 * </pre> 033 * <p> 034 * This class was copied from the 035 * <a href="http://svn.apache.org/repos/asf/jackrabbit/trunk/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ISO8601.java">Apache SVN</a>. 036 * It is free software under the Apache Licence. 037 */ 038public final class ISO8601 { 039 040 /** 041 * Convenience method calling {@link #parse(String)} and converting the 042 * result to a {@link Date}. 043 * @param text the text to be parsed. Must not be <code>null</code>. 044 * @return the parsed {@link Date}. Never <code>null</code>. 045 */ 046 public static Date parseDate(String text) { 047 Calendar calendar = parse(text); 048 return calendar.getTime(); 049 } 050 051 /** 052 * Convenience method calling {@link #format(Calendar)} with a UTC 053 * time zone (as a {@link Date} does not have a time zone by definition). 054 */ 055 public static String formatDate(Date date) throws IllegalArgumentException { 056 if (date == null) { 057 throw new IllegalArgumentException("argument can not be null"); 058 } 059 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.UK); 060 calendar.setTime(date); 061 return format(calendar); 062 } 063 064 /** 065 * Parses an ISO8601-compliant date/time string. 066 * 067 * @param text the date/time string to be parsed 068 * @return a <code>Calendar</code>, or <code>null</code> if the input could 069 * not be parsed 070 * @throws IllegalArgumentException if a <code>null</code> argument is passed 071 */ 072 public static Calendar parse(String text) { 073 if (text == null) { 074 throw new IllegalArgumentException("argument can not be null"); 075 } 076 077 // check optional leading sign 078 char sign; 079 int start; 080 if (text.startsWith("-")) { 081 sign = '-'; 082 start = 1; 083 } else if (text.startsWith("+")) { 084 sign = '+'; 085 start = 1; 086 } else { 087 sign = '+'; // no sign specified, implied '+' 088 start = 0; 089 } 090 091 /** 092 * the expected format of the remainder of the string is: 093 * YYYY-MM-DDThh:mm:ss.SSSTZD 094 * 095 * note that we cannot use java.text.SimpleDateFormat for 096 * parsing because it can't handle years <= 0 and TZD's 097 */ 098 099 int year, month, day, hour, min, sec, ms; 100 String tzID; 101 try { 102 // year (YYYY) 103 year = Integer.parseInt(text.substring(start, start + 4)); 104 start += 4; 105 // delimiter '-' 106 if (text.charAt(start) != '-') { 107 return null; 108 } 109 start++; 110 // month (MM) 111 month = Integer.parseInt(text.substring(start, start + 2)); 112 start += 2; 113 // delimiter '-' 114 if (text.charAt(start) != '-') { 115 return null; 116 } 117 start++; 118 // day (DD) 119 day = Integer.parseInt(text.substring(start, start + 2)); 120 start += 2; 121 // delimiter 'T' 122 if (text.charAt(start) != 'T') { 123 return null; 124 } 125 start++; 126 // hour (hh) 127 hour = Integer.parseInt(text.substring(start, start + 2)); 128 start += 2; 129 // delimiter ':' 130 if (text.charAt(start) != ':') { 131 return null; 132 } 133 start++; 134 // minute (mm) 135 min = Integer.parseInt(text.substring(start, start + 2)); 136 start += 2; 137 // delimiter ':' 138 if (text.charAt(start) != ':') { 139 return null; 140 } 141 start++; 142 // second (ss) 143 sec = Integer.parseInt(text.substring(start, start + 2)); 144 start += 2; 145 // delimiter '.' 146 if (text.charAt(start) != '.') { 147 return null; 148 } 149 start++; 150 // millisecond (SSS) 151 ms = Integer.parseInt(text.substring(start, start + 3)); 152 start += 3; 153 // time zone designator (Z or +00:00 or -00:00) 154 if (text.charAt(start) == '+' || text.charAt(start) == '-') { 155 // offset to UTC specified in the format +00:00/-00:00 156 tzID = "GMT" + text.substring(start); 157 } else if (text.substring(start).equals("Z")) { 158 tzID = "GMT"; 159 } else { 160 // invalid time zone designator 161 return null; 162 } 163 } catch (IndexOutOfBoundsException e) { 164 return null; 165 } catch (NumberFormatException e) { 166 return null; 167 } 168 169 TimeZone tz = TimeZone.getTimeZone(tzID); 170 // verify id of returned time zone (getTimeZone defaults to "GMT") 171 if (!tz.getID().equals(tzID)) { 172 // invalid time zone 173 return null; 174 } 175 176 // initialize Calendar object 177 Calendar cal = Calendar.getInstance(tz); 178 cal.setLenient(false); 179 // year and era 180 if (sign == '-' || year == 0) { 181 // not CE, need to set era (BCE) and adjust year 182 cal.set(Calendar.YEAR, year + 1); 183 cal.set(Calendar.ERA, GregorianCalendar.BC); 184 } else { 185 cal.set(Calendar.YEAR, year); 186 cal.set(Calendar.ERA, GregorianCalendar.AD); 187 } 188 // month (0-based!) 189 cal.set(Calendar.MONTH, month - 1); 190 // day of month 191 cal.set(Calendar.DAY_OF_MONTH, day); 192 // hour 193 cal.set(Calendar.HOUR_OF_DAY, hour); 194 // minute 195 cal.set(Calendar.MINUTE, min); 196 // second 197 cal.set(Calendar.SECOND, sec); 198 // millisecond 199 cal.set(Calendar.MILLISECOND, ms); 200 201 try { 202 /** 203 * the following call will trigger an IllegalArgumentException 204 * if any of the set values are illegal or out of range 205 */ 206 cal.getTime(); 207 /** 208 * in addition check the validity of the year 209 */ 210 getYear(cal); 211 } catch (IllegalArgumentException e) { 212 return null; 213 } 214 215 return cal; 216 } 217 218 /** 219 * Formats a <code>Calendar</code> value into an ISO8601-compliant 220 * date/time string. 221 * 222 * @param cal the time value to be formatted into a date/time string. 223 * @return the formatted date/time string. 224 * @throws IllegalArgumentException if a <code>null</code> argument is passed 225 * or the calendar cannot be represented as defined by ISO 8601 (i.e. year 226 * with more than four digits). 227 */ 228 public static String format(Calendar cal) throws IllegalArgumentException { 229 if (cal == null) { 230 throw new IllegalArgumentException("argument can not be null"); 231 } 232 233 /** 234 * the format of the date/time string is: 235 * YYYY-MM-DDThh:mm:ss.SSSTZD 236 * 237 * note that we cannot use java.text.SimpleDateFormat for 238 * formatting because it can't handle years <= 0 and TZD's 239 */ 240 StringBuilder buf = new StringBuilder(); 241 // year ([-]YYYY) 242 appendZeroPaddedInt(buf, getYear(cal), 4); 243 buf.append('-'); 244 // month (MM) 245 appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2); 246 buf.append('-'); 247 // day (DD) 248 appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2); 249 buf.append('T'); 250 // hour (hh) 251 appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2); 252 buf.append(':'); 253 // minute (mm) 254 appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2); 255 buf.append(':'); 256 // second (ss) 257 appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2); 258 buf.append('.'); 259 // millisecond (SSS) 260 appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3); 261 // time zone designator (Z or +00:00 or -00:00) 262 TimeZone tz = cal.getTimeZone(); 263 // determine offset of timezone from UTC (incl. daylight saving) 264 int offset = tz.getOffset(cal.getTimeInMillis()); 265 if (offset != 0) { 266 int hours = Math.abs((offset / (60 * 1000)) / 60); 267 int minutes = Math.abs((offset / (60 * 1000)) % 60); 268 buf.append(offset < 0 ? '-' : '+'); 269 appendZeroPaddedInt(buf, hours, 2); 270 buf.append(':'); 271 appendZeroPaddedInt(buf, minutes, 2); 272 } else { 273 buf.append('Z'); 274 } 275 return buf.toString(); 276 } 277 278 /** 279 * Returns the astronomical year of the given calendar. 280 * 281 * @param cal a calendar instance. 282 * @return the astronomical year. 283 * @throws IllegalArgumentException if calendar cannot be represented as 284 * defined by ISO 8601 (i.e. year with more 285 * than four digits). 286 */ 287 public static int getYear(Calendar cal) throws IllegalArgumentException { 288 // determine era and adjust year if necessary 289 int year = cal.get(Calendar.YEAR); 290 if (cal.isSet(Calendar.ERA) 291 && cal.get(Calendar.ERA) == GregorianCalendar.BC) { 292 /** 293 * calculate year using astronomical system: 294 * year n BCE => astronomical year -n + 1 295 */ 296 year = 0 - year + 1; 297 } 298 299 if (year > 9999 || year < -9999) { 300 throw new IllegalArgumentException("Calendar has more than four " + 301 "year digits, cannot be formatted as ISO8601: " + year); 302 } 303 return year; 304 } 305 306 /** 307 * Appends a zero-padded number to the given string buffer. 308 * <p/> 309 * This is an internal helper method which doesn't perform any 310 * validation on the given arguments. 311 * 312 * @param buf String buffer to append to 313 * @param n number to append 314 * @param precision number of digits to append 315 */ 316 private static void appendZeroPaddedInt(StringBuilder buf, int n, int precision) { 317 if (n < 0) { 318 buf.append('-'); 319 n = -n; 320 } 321 322 for (int exp = precision - 1; exp > 0; exp--) { 323 if (n < Math.pow(10, exp)) { 324 buf.append('0'); 325 } else { 326 break; 327 } 328 } 329 buf.append(n); 330 } 331}