001 package com.thaiopensource.datatype.xsd; 002 003 import org.relaxng.datatype.ValidationContext; 004 005 import java.util.Calendar; 006 import java.util.Date; 007 import java.util.GregorianCalendar; 008 009 class DateTimeDatatype extends RegexDatatype implements OrderRelation { 010 static private final String YEAR_PATTERN = "-?([1-9][0-9]*)?[0-9]{4}"; 011 static private final String MONTH_PATTERN = "[0-9]{2}"; 012 static private final String DAY_OF_MONTH_PATTERN = "[0-9]{2}"; 013 static private final String TIME_PATTERN = "[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]*)?"; 014 static private final String TZ_PATTERN = "(Z|[+\\-][0-9][0-9]:[0-5][0-9])?"; 015 016 private final String template; 017 018 /** 019 * The argument specifies the lexical representation accepted: 020 * Y specifies a year with optional preceding minus 021 * M specifies a two digit month 022 * D specifies a two digit day of month 023 * t specifies a time (hh:mm:ss.sss) 024 * any other character stands for itself. 025 * All lexical representations are implicitly followed by an optional time zone. 026 */ 027 DateTimeDatatype(String template) { 028 super(makePattern(template)); 029 this.template = template; 030 } 031 032 static private String makePattern(String template) { 033 StringBuffer pattern = new StringBuffer(); 034 for (int i = 0, len = template.length(); i < len; i++) { 035 char c = template.charAt(i); 036 switch (c) { 037 case 'Y': 038 pattern.append(YEAR_PATTERN); 039 break; 040 case 'M': 041 pattern.append(MONTH_PATTERN); 042 break; 043 case 'D': 044 pattern.append(DAY_OF_MONTH_PATTERN); 045 break; 046 case 't': 047 pattern.append(TIME_PATTERN); 048 break; 049 default: 050 pattern.append(c); 051 break; 052 } 053 } 054 pattern.append(TZ_PATTERN); 055 return pattern.toString(); 056 } 057 058 boolean allowsValue(String str, ValidationContext vc) { 059 return getValue(str, vc) != null; 060 } 061 062 static private class DateTime { 063 private final Date date; 064 private final int leapMilliseconds; 065 private final boolean hasTimeZone; 066 067 DateTime(Date date, int leapMilliseconds, boolean hasTimeZone) { 068 this.date = date; 069 this.leapMilliseconds = leapMilliseconds; 070 this.hasTimeZone = hasTimeZone; 071 } 072 073 public boolean equals(Object obj) { 074 if (!(obj instanceof DateTime)) 075 return false; 076 DateTime other = (DateTime)obj; 077 return (this.date.equals(other.date) 078 && this.leapMilliseconds == other.leapMilliseconds 079 && this.hasTimeZone == other.hasTimeZone); 080 } 081 082 public int hashCode() { 083 return date.hashCode(); 084 } 085 086 Date getDate() { 087 return date; 088 } 089 090 int getLeapMilliseconds() { 091 return leapMilliseconds; 092 } 093 094 boolean getHasTimeZone() { 095 return hasTimeZone; 096 } 097 } 098 099 // XXX Check leap second validity? 100 // XXX Allow 24:00:00? 101 Object getValue(String str, ValidationContext vc) { 102 boolean negative = false; 103 int year = 2000; // any leap year will do 104 int month = 1; 105 int day = 1; 106 int hours = 0; 107 int minutes = 0; 108 int seconds = 0; 109 int milliseconds = 0; 110 int pos = 0; 111 int len = str.length(); 112 for (int templateIndex = 0, templateLength = template.length(); 113 templateIndex < templateLength; 114 templateIndex++) { 115 char templateChar = template.charAt(templateIndex); 116 switch (templateChar) { 117 case 'Y': 118 negative = str.charAt(pos) == '-'; 119 int yearStartIndex = negative ? pos + 1 : pos; 120 pos = skipDigits(str, yearStartIndex); 121 try { 122 year = Integer.parseInt(str.substring(yearStartIndex, pos)); 123 } 124 catch (NumberFormatException e) { 125 return null; 126 } 127 break; 128 case 'M': 129 month = parse2Digits(str, pos); 130 pos += 2; 131 break; 132 case 'D': 133 day = parse2Digits(str, pos); 134 pos += 2; 135 break; 136 case 't': 137 hours = parse2Digits(str, pos); 138 pos += 3; 139 minutes = parse2Digits(str, pos); 140 pos += 3; 141 seconds = parse2Digits(str, pos); 142 pos += 2; 143 if (pos < len && str.charAt(pos) == '.') { 144 int end = skipDigits(str, ++pos); 145 for (int j = 0; j < 3; j++) { 146 milliseconds *= 10; 147 if (pos < end) 148 milliseconds += str.charAt(pos++) - '0'; 149 } 150 pos = end; 151 } 152 break; 153 default: 154 pos++; 155 break; 156 } 157 } 158 boolean hasTimeZone = pos < len; 159 int tzOffset; 160 if (hasTimeZone && str.charAt(pos) != 'Z') 161 tzOffset = parseTimeZone(str, pos); 162 else 163 tzOffset = 0; 164 int leapMilliseconds; 165 if (seconds == 60) { 166 leapMilliseconds = milliseconds + 1; 167 milliseconds = 999; 168 seconds = 59; 169 } 170 else 171 leapMilliseconds = 0; 172 try { 173 GregorianCalendar cal = CalendarFactory.getCalendar(); 174 Date date; 175 if (cal == CalendarFactory.cal) { 176 synchronized (cal) { 177 date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds); 178 } 179 } 180 else 181 date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds); 182 return new DateTime(date, leapMilliseconds, hasTimeZone); 183 } 184 catch (IllegalArgumentException e) { 185 return null; 186 } 187 } 188 189 // The GregorianCalendar constructor is incredibly slow with some 190 // versions of GCJ (specifically the version shipped with RedHat 9). 191 // This code attempts to detect when construction is slow. 192 // When it is, we synchronize access to a single 193 // object; otherwise, we create a new object each time we need it 194 // so as to avoid thread lock contention. 195 196 static class CalendarFactory { 197 static private final int UNKNOWN = -1; 198 static private final int SLOW = 0; 199 static private final int FAST = 1; 200 static private final int LIMIT = 10; 201 static private int speed = UNKNOWN; 202 static GregorianCalendar cal = new GregorianCalendar(); 203 204 static GregorianCalendar getCalendar() { 205 // Don't need to synchronize this because speed is atomic. 206 switch (speed) { 207 case SLOW: 208 return cal; 209 case FAST: 210 return new GregorianCalendar(); 211 } 212 // Note that we are not timing the first construction (which happens 213 // at class initialization), since that may involve one-time cache 214 // initialization. 215 long start = System.currentTimeMillis(); 216 GregorianCalendar tem = new GregorianCalendar(); 217 long time = System.currentTimeMillis() - start; 218 speed = time > LIMIT ? SLOW : FAST; 219 return tem; 220 } 221 } 222 223 private static Date createDate(GregorianCalendar cal, int tzOffset, boolean negative, 224 int year, int month, int day, 225 int hours, int minutes, int seconds, int milliseconds) { 226 cal.setLenient(false); 227 cal.setGregorianChange(new Date(Long.MIN_VALUE)); 228 cal.clear(); 229 // Using a time zone of "GMT+XX:YY" doesn't work with JDK 1.1, so we have to do it like this. 230 cal.set(Calendar.ZONE_OFFSET, tzOffset); 231 cal.set(Calendar.DST_OFFSET, 0); 232 cal.set(Calendar.ERA, negative ? GregorianCalendar.BC : GregorianCalendar.AD); 233 // months in ISO8601 start with 1; months in Java start with 0 234 month -= 1; 235 cal.set(year, month, day, hours, minutes, seconds); 236 cal.set(Calendar.MILLISECOND, milliseconds); 237 checkDate(cal.isLeapYear(year), month, day); // for GCJ 238 return cal.getTime(); 239 } 240 241 static private void checkDate(boolean isLeapYear, int month, int day) { 242 if (month < 0 || month > 11 || day < 1) 243 throw new IllegalArgumentException(); 244 int dayMax; 245 switch (month) { 246 // Thirty days have September, April, June and November... 247 case Calendar.SEPTEMBER: 248 case Calendar.APRIL: 249 case Calendar.JUNE: 250 case Calendar.NOVEMBER: 251 dayMax = 30; 252 break; 253 case Calendar.FEBRUARY: 254 dayMax = isLeapYear ? 29 : 28; 255 break; 256 default: 257 dayMax = 31; 258 break; 259 } 260 if (day > dayMax) 261 throw new IllegalArgumentException(); 262 } 263 264 static private int parseTimeZone(String str, int i) { 265 int sign = str.charAt(i) == '-' ? -1 : 1; 266 return (Integer.parseInt(str.substring(i + 1, i + 3))*60 + Integer.parseInt(str.substring(i + 4)))*60*1000*sign; 267 } 268 269 static private int parse2Digits(String str, int i) { 270 return (str.charAt(i) - '0')*10 + (str.charAt(i + 1) - '0'); 271 } 272 273 static private int skipDigits(String str, int i) { 274 for (int len = str.length(); i < len; i++) { 275 if ("0123456789".indexOf(str.charAt(i)) < 0) 276 break; 277 } 278 return i; 279 } 280 281 OrderRelation getOrderRelation() { 282 return this; 283 } 284 285 static private final int TIME_ZONE_MAX = 14*60*60*1000; 286 287 public boolean isLessThan(Object obj1, Object obj2) { 288 DateTime dt1 = (DateTime)obj1; 289 DateTime dt2 = (DateTime)obj2; 290 long t1 = dt1.getDate().getTime(); 291 long t2 = dt2.getDate().getTime(); 292 if (dt1.getHasTimeZone() == dt2.getHasTimeZone()) 293 return isLessThan(t1, 294 dt1.getLeapMilliseconds(), 295 t2, 296 dt2.getLeapMilliseconds()); 297 else if (!dt2.getHasTimeZone()) 298 return isLessThan(t1, dt1.getLeapMilliseconds(), t2 - TIME_ZONE_MAX, dt2.getLeapMilliseconds()); 299 else 300 return isLessThan(t1 + TIME_ZONE_MAX, dt1.getLeapMilliseconds(), t2, dt2.getLeapMilliseconds()); 301 } 302 303 static private boolean isLessThan(long t1, int leapMillis1, long t2, int leapMillis2) { 304 if (t1 < t2) 305 return true; 306 if (t1 > t2) 307 return false; 308 if (leapMillis1 < leapMillis2) 309 return true; 310 return false; 311 } 312 }