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 }