1
package com.thaiopensource.datatype.xsd;
3
import org.relaxng.datatype.DatatypeException;
4
import org.relaxng.datatype.ValidationContext;
6
import java.util.Calendar;
8
import java.util.GregorianCalendar;
10
class DateTimeDatatype extends RegexDatatype implements OrderRelation {
11
static private final String YEAR_PATTERN = "-?([1-9][0-9]*)?[0-9]{4}";
12
static private final String MONTH_PATTERN = "[0-9]{2}";
13
static private final String DAY_OF_MONTH_PATTERN = "[0-9]{2}";
14
static private final String TIME_PATTERN = "[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]*)?";
15
static private final String TZ_PATTERN = "(Z|[+\\-][0-9][0-9]:[0-5][0-9])?";
17
private final String template;
18
private final String lexicalSpaceKey;
21
* The argument specifies the lexical representation accepted:
22
* Y specifies a year with optional preceding minus
23
* M specifies a two digit month
24
* D specifies a two digit day of month
25
* t specifies a time (hh:mm:ss.sss)
26
* any other character stands for itself.
27
* All lexical representations are implicitly followed by an optional time zone.
29
DateTimeDatatype(String template) {
30
super(makePattern(template));
31
this.template = template;
32
this.lexicalSpaceKey = makeLexicalSpaceKey(template);
35
String getLexicalSpaceKey() {
36
return lexicalSpaceKey;
39
static private String makeLexicalSpaceKey(String template) {
41
if (template.indexOf('Y') >= 0)
43
if (template.indexOf('M') >= 0)
45
if (template.indexOf('D') >= 0)
49
if (template.indexOf('t') >= 0)
50
key = key.length() > 0 ? key + "_time" : "time";
54
static private String makePattern(String template) {
55
StringBuffer pattern = new StringBuffer();
56
for (int i = 0, len = template.length(); i < len; i++) {
57
char c = template.charAt(i);
60
pattern.append(YEAR_PATTERN);
63
pattern.append(MONTH_PATTERN);
66
pattern.append(DAY_OF_MONTH_PATTERN);
69
pattern.append(TIME_PATTERN);
76
pattern.append(TZ_PATTERN);
77
return pattern.toString();
80
static private class DateTime {
81
private final Date date;
82
private final int leapMilliseconds;
83
private final boolean hasTimeZone;
85
DateTime(Date date, int leapMilliseconds, boolean hasTimeZone) {
87
this.leapMilliseconds = leapMilliseconds;
88
this.hasTimeZone = hasTimeZone;
91
public boolean equals(Object obj) {
92
if (!(obj instanceof DateTime))
94
DateTime other = (DateTime)obj;
95
return (this.date.equals(other.date)
96
&& this.leapMilliseconds == other.leapMilliseconds
97
&& this.hasTimeZone == other.hasTimeZone);
100
public int hashCode() {
101
return date.hashCode();
108
int getLeapMilliseconds() {
109
return leapMilliseconds;
112
boolean getHasTimeZone() {
117
// XXX Check leap second validity?
118
// XXX Allow 24:00:00?
119
Object getValue(String str, ValidationContext vc) throws DatatypeException {
120
boolean negative = false;
121
int year = 2000; // any leap year will do
127
int milliseconds = 0;
129
int len = str.length();
130
for (int templateIndex = 0, templateLength = template.length();
131
templateIndex < templateLength;
133
char templateChar = template.charAt(templateIndex);
134
switch (templateChar) {
136
negative = str.charAt(pos) == '-';
137
int yearStartIndex = negative ? pos + 1 : pos;
138
pos = skipDigits(str, yearStartIndex);
140
year = Integer.parseInt(str.substring(yearStartIndex, pos));
142
catch (NumberFormatException e) {
143
throw createLexicallyInvalidException();
147
month = parse2Digits(str, pos);
151
day = parse2Digits(str, pos);
155
hours = parse2Digits(str, pos);
157
minutes = parse2Digits(str, pos);
159
seconds = parse2Digits(str, pos);
161
if (pos < len && str.charAt(pos) == '.') {
162
int end = skipDigits(str, ++pos);
163
for (int j = 0; j < 3; j++) {
166
milliseconds += str.charAt(pos++) - '0';
176
boolean hasTimeZone = pos < len;
178
if (hasTimeZone && str.charAt(pos) != 'Z')
179
tzOffset = parseTimeZone(str, pos);
182
int leapMilliseconds;
184
leapMilliseconds = milliseconds + 1;
189
leapMilliseconds = 0;
191
GregorianCalendar cal = CalendarFactory.getCalendar();
193
if (cal == CalendarFactory.cal) {
195
date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds);
199
date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds);
200
return new DateTime(date, leapMilliseconds, hasTimeZone);
202
catch (IllegalArgumentException e) {
203
throw createLexicallyInvalidException();
207
// The GregorianCalendar constructor is incredibly slow with some
208
// versions of GCJ (specifically the version shipped with RedHat 9).
209
// This code attempts to detect when construction is slow.
210
// When it is, we synchronize access to a single
211
// object; otherwise, we create a new object each time we need it
212
// so as to avoid thread lock contention.
214
static class CalendarFactory {
215
static private final int UNKNOWN = -1;
216
static private final int SLOW = 0;
217
static private final int FAST = 1;
218
static private final int LIMIT = 10;
219
static private int speed = UNKNOWN;
220
static GregorianCalendar cal = new GregorianCalendar();
222
static GregorianCalendar getCalendar() {
223
// Don't need to synchronize this because speed is atomic.
228
return new GregorianCalendar();
230
// Note that we are not timing the first construction (which happens
231
// at class initialization), since that may involve one-time cache
233
long start = System.currentTimeMillis();
234
GregorianCalendar tem = new GregorianCalendar();
235
long time = System.currentTimeMillis() - start;
236
speed = time > LIMIT ? SLOW : FAST;
241
private static Date createDate(GregorianCalendar cal, int tzOffset, boolean negative,
242
int year, int month, int day,
243
int hours, int minutes, int seconds, int milliseconds) {
244
cal.setLenient(false);
245
cal.setGregorianChange(new Date(Long.MIN_VALUE));
247
// Using a time zone of "GMT+XX:YY" doesn't work with JDK 1.1, so we have to do it like this.
248
cal.set(Calendar.ZONE_OFFSET, tzOffset);
249
cal.set(Calendar.DST_OFFSET, 0);
250
cal.set(Calendar.ERA, negative ? GregorianCalendar.BC : GregorianCalendar.AD);
251
// months in ISO8601 start with 1; months in Java start with 0
253
cal.set(year, month, day, hours, minutes, seconds);
254
cal.set(Calendar.MILLISECOND, milliseconds);
255
checkDate(cal.isLeapYear(year), month, day); // for GCJ
256
return cal.getTime();
259
static private void checkDate(boolean isLeapYear, int month, int day) {
260
if (month < 0 || month > 11 || day < 1)
261
throw new IllegalArgumentException();
264
// Thirty days have September, April, June and November...
265
case Calendar.SEPTEMBER:
268
case Calendar.NOVEMBER:
271
case Calendar.FEBRUARY:
272
dayMax = isLeapYear ? 29 : 28;
279
throw new IllegalArgumentException();
282
static private int parseTimeZone(String str, int i) {
283
int sign = str.charAt(i) == '-' ? -1 : 1;
284
return (Integer.parseInt(str.substring(i + 1, i + 3))*60 + Integer.parseInt(str.substring(i + 4)))*60*1000*sign;
287
static private int parse2Digits(String str, int i) {
288
return (str.charAt(i) - '0')*10 + (str.charAt(i + 1) - '0');
291
static private int skipDigits(String str, int i) {
292
for (int len = str.length(); i < len; i++) {
293
if ("0123456789".indexOf(str.charAt(i)) < 0)
299
OrderRelation getOrderRelation() {
303
static private final int TIME_ZONE_MAX = 14*60*60*1000;
305
public boolean isLessThan(Object obj1, Object obj2) {
306
DateTime dt1 = (DateTime)obj1;
307
DateTime dt2 = (DateTime)obj2;
308
long t1 = dt1.getDate().getTime();
309
long t2 = dt2.getDate().getTime();
310
if (dt1.getHasTimeZone() == dt2.getHasTimeZone())
311
return isLessThan(t1,
312
dt1.getLeapMilliseconds(),
314
dt2.getLeapMilliseconds());
315
else if (!dt2.getHasTimeZone())
316
return isLessThan(t1, dt1.getLeapMilliseconds(), t2 - TIME_ZONE_MAX, dt2.getLeapMilliseconds());
318
return isLessThan(t1 + TIME_ZONE_MAX, dt1.getLeapMilliseconds(), t2, dt2.getLeapMilliseconds());
321
static private boolean isLessThan(long t1, int leapMillis1, long t2, int leapMillis2) {
326
if (leapMillis1 < leapMillis2)