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    }