package com.thaiopensource.datatype.xsd;
import org.relaxng.datatype.DatatypeException;
import org.relaxng.datatype.ValidationContext;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
class DateTimeDatatype extends RegexDatatype implements OrderRelation {
static private final String YEAR_PATTERN = "-?([1-9][0-9]*)?[0-9]{4}";
static private final String MONTH_PATTERN = "[0-9]{2}";
static private final String DAY_OF_MONTH_PATTERN = "[0-9]{2}";
static private final String TIME_PATTERN = "[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]*)?";
static private final String TZ_PATTERN = "(Z|[+\\-][0-9][0-9]:[0-5][0-9])?";
private final String template;
private final String lexicalSpaceKey;
/**
* The argument specifies the lexical representation accepted:
* Y specifies a year with optional preceding minus
* M specifies a two digit month
* D specifies a two digit day of month
* t specifies a time (hh:mm:ss.sss)
* any other character stands for itself.
* All lexical representations are implicitly followed by an optional time zone.
*/
DateTimeDatatype(String template) {
super(makePattern(template));
this.template = template;
this.lexicalSpaceKey = makeLexicalSpaceKey(template);
}
String getLexicalSpaceKey() {
return lexicalSpaceKey;
}
static private String makeLexicalSpaceKey(String template) {
String key = "";
if (template.indexOf('Y') >= 0)
key += "_y";
if (template.indexOf('M') >= 0)
key += "_m";
if (template.indexOf('D') >= 0)
key += "_d";
if (key.length() > 0)
key = "date" + key;
if (template.indexOf('t') >= 0)
key = key.length() > 0 ? key + "_time" : "time";
return key;
}
static private String makePattern(String template) {
StringBuffer pattern = new StringBuffer();
for (int i = 0, len = template.length(); i < len; i++) {
char c = template.charAt(i);
switch (c) {
case 'Y':
pattern.append(YEAR_PATTERN);
break;
case 'M':
pattern.append(MONTH_PATTERN);
break;
case 'D':
pattern.append(DAY_OF_MONTH_PATTERN);
break;
case 't':
pattern.append(TIME_PATTERN);
break;
default:
pattern.append(c);
break;
}
}
pattern.append(TZ_PATTERN);
return pattern.toString();
}
static private class DateTime {
private final Date date;
private final int leapMilliseconds;
private final boolean hasTimeZone;
DateTime(Date date, int leapMilliseconds, boolean hasTimeZone) {
this.date = date;
this.leapMilliseconds = leapMilliseconds;
this.hasTimeZone = hasTimeZone;
}
public boolean equals(Object obj) {
if (!(obj instanceof DateTime))
return false;
DateTime other = (DateTime)obj;
return (this.date.equals(other.date)
&& this.leapMilliseconds == other.leapMilliseconds
&& this.hasTimeZone == other.hasTimeZone);
}
public int hashCode() {
return date.hashCode();
}
Date getDate() {
return date;
}
int getLeapMilliseconds() {
return leapMilliseconds;
}
boolean getHasTimeZone() {
return hasTimeZone;
}
}
// XXX Check leap second validity?
// XXX Allow 24:00:00?
Object getValue(String str, ValidationContext vc) throws DatatypeException {
boolean negative = false;
int year = 2000; // any leap year will do
int month = 1;
int day = 1;
int hours = 0;
int minutes = 0;
int seconds = 0;
int milliseconds = 0;
int pos = 0;
int len = str.length();
for (int templateIndex = 0, templateLength = template.length();
templateIndex < templateLength;
templateIndex++) {
char templateChar = template.charAt(templateIndex);
switch (templateChar) {
case 'Y':
negative = str.charAt(pos) == '-';
int yearStartIndex = negative ? pos + 1 : pos;
pos = skipDigits(str, yearStartIndex);
try {
year = Integer.parseInt(str.substring(yearStartIndex, pos));
}
catch (NumberFormatException e) {
throw createLexicallyInvalidException();
}
break;
case 'M':
month = parse2Digits(str, pos);
pos += 2;
break;
case 'D':
day = parse2Digits(str, pos);
pos += 2;
break;
case 't':
hours = parse2Digits(str, pos);
pos += 3;
minutes = parse2Digits(str, pos);
pos += 3;
seconds = parse2Digits(str, pos);
pos += 2;
if (pos < len && str.charAt(pos) == '.') {
int end = skipDigits(str, ++pos);
for (int j = 0; j < 3; j++) {
milliseconds *= 10;
if (pos < end)
milliseconds += str.charAt(pos++) - '0';
}
pos = end;
}
break;
default:
pos++;
break;
}
}
boolean hasTimeZone = pos < len;
int tzOffset;
if (hasTimeZone && str.charAt(pos) != 'Z')
tzOffset = parseTimeZone(str, pos);
else
tzOffset = 0;
int leapMilliseconds;
if (seconds == 60) {
leapMilliseconds = milliseconds + 1;
milliseconds = 999;
seconds = 59;
}
else
leapMilliseconds = 0;
try {
GregorianCalendar cal = CalendarFactory.getCalendar();
Date date;
if (cal == CalendarFactory.cal) {
synchronized (cal) {
date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds);
}
}
else
date = createDate(cal, tzOffset, negative, year, month, day, hours, minutes, seconds, milliseconds);
return new DateTime(date, leapMilliseconds, hasTimeZone);
}
catch (IllegalArgumentException e) {
throw createLexicallyInvalidException();
}
}
// The GregorianCalendar constructor is incredibly slow with some
// versions of GCJ (specifically the version shipped with RedHat 9).
// This code attempts to detect when construction is slow.
// When it is, we synchronize access to a single
// object; otherwise, we create a new object each time we need it
// so as to avoid thread lock contention.
static class CalendarFactory {
static private final int UNKNOWN = -1;
static private final int SLOW = 0;
static private final int FAST = 1;
static private final int LIMIT = 10;
static private int speed = UNKNOWN;
static GregorianCalendar cal = new GregorianCalendar();
static GregorianCalendar getCalendar() {
// Don't need to synchronize this because speed is atomic.
switch (speed) {
case SLOW:
return cal;
case FAST:
return new GregorianCalendar();
}
// Note that we are not timing the first construction (which happens
// at class initialization), since that may involve one-time cache
// initialization.
long start = System.currentTimeMillis();
GregorianCalendar tem = new GregorianCalendar();
long time = System.currentTimeMillis() - start;
speed = time > LIMIT ? SLOW : FAST;
return tem;
}
}
private static Date createDate(GregorianCalendar cal, int tzOffset, boolean negative,
int year, int month, int day,
int hours, int minutes, int seconds, int milliseconds) {
cal.setLenient(false);
cal.setGregorianChange(new Date(Long.MIN_VALUE));
cal.clear();
// Using a time zone of "GMT+XX:YY" doesn't work with JDK 1.1, so we have to do it like this.
cal.set(Calendar.ZONE_OFFSET, tzOffset);
cal.set(Calendar.DST_OFFSET, 0);
cal.set(Calendar.ERA, negative ? GregorianCalendar.BC : GregorianCalendar.AD);
// months in ISO8601 start with 1; months in Java start with 0
month -= 1;
cal.set(year, month, day, hours, minutes, seconds);
cal.set(Calendar.MILLISECOND, milliseconds);
checkDate(cal.isLeapYear(year), month, day); // for GCJ
return cal.getTime();
}
static private void checkDate(boolean isLeapYear, int month, int day) {
if (month < 0 || month > 11 || day < 1)
throw new IllegalArgumentException();
int dayMax;
switch (month) {
// Thirty days have September, April, June and November...
case Calendar.SEPTEMBER:
case Calendar.APRIL:
case Calendar.JUNE:
case Calendar.NOVEMBER:
dayMax = 30;
break;
case Calendar.FEBRUARY:
dayMax = isLeapYear ? 29 : 28;
break;
default:
dayMax = 31;
break;
}
if (day > dayMax)
throw new IllegalArgumentException();
}
static private int parseTimeZone(String str, int i) {
int sign = str.charAt(i) == '-' ? -1 : 1;
return (Integer.parseInt(str.substring(i + 1, i + 3))*60 + Integer.parseInt(str.substring(i + 4)))*60*1000*sign;
}
static private int parse2Digits(String str, int i) {
return (str.charAt(i) - '0')*10 + (str.charAt(i + 1) - '0');
}
static private int skipDigits(String str, int i) {
for (int len = str.length(); i < len; i++) {
if ("0123456789".indexOf(str.charAt(i)) < 0)
break;
}
return i;
}
OrderRelation getOrderRelation() {
return this;
}
static private final int TIME_ZONE_MAX = 14*60*60*1000;
public boolean isLessThan(Object obj1, Object obj2) {
DateTime dt1 = (DateTime)obj1;
DateTime dt2 = (DateTime)obj2;
long t1 = dt1.getDate().getTime();
long t2 = dt2.getDate().getTime();
if (dt1.getHasTimeZone() == dt2.getHasTimeZone())
return isLessThan(t1,
dt1.getLeapMilliseconds(),
t2,
dt2.getLeapMilliseconds());
else if (!dt2.getHasTimeZone())
return isLessThan(t1, dt1.getLeapMilliseconds(), t2 - TIME_ZONE_MAX, dt2.getLeapMilliseconds());
else
return isLessThan(t1 + TIME_ZONE_MAX, dt1.getLeapMilliseconds(), t2, dt2.getLeapMilliseconds());
}
static private boolean isLessThan(long t1, int leapMillis1, long t2, int leapMillis2) {
if (t1 < t2)
return true;
if (t1 > t2)
return false;
if (leapMillis1 < leapMillis2)
return true;
return false;
}
}