View Javadoc
1   /*
2    * reserved comment block
3    * DO NOT REMOVE OR ALTER!
4    */
5   /*
6    * Copyright 1999-2005 The Apache Software Foundation.
7    *
8    * Licensed under the Apache License, Version 2.0 (the "License");
9    * you may not use this file except in compliance with the License.
10   * You may obtain a copy of the License at
11   *
12   *      http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  
21  package com.sun.org.apache.xerces.internal.impl.dv.xs;
22  
23  import java.math.BigDecimal;
24  
25  import javax.xml.datatype.DatatypeFactory;
26  import javax.xml.datatype.Duration;
27  import javax.xml.datatype.XMLGregorianCalendar;
28  
29  import com.sun.org.apache.xerces.internal.impl.Constants;
30  import com.sun.org.apache.xerces.internal.jaxp.datatype.DatatypeFactoryImpl;
31  import com.sun.org.apache.xerces.internal.xs.datatypes.XSDateTime;
32  
33  /**
34   * This is the base class of all date/time datatype validators.
35   * It implements common code for parsing, validating and comparing datatypes.
36   * Classes that extend this class, must implement parse() method.
37   *
38   * REVISIT: There are many instance variables, which would cause problems
39   *          when we support grammar caching. A grammar is possibly used by
40   *          two parser instances at the same time, then the same simple type
41   *          decl object can be used to validate two strings at the same time.
42   *          -SG
43   *
44   * @xerces.internal
45   *
46   * @author Elena Litani
47   * @author Len Berman
48   * @author Gopal Sharma, SUN Microsystems Inc.
49   *
50   * @version $Id: AbstractDateTimeDV.java,v 1.7 2010-11-01 04:39:46 joehw Exp $
51   */
52  public abstract class AbstractDateTimeDV extends TypeValidator {
53  
54      //debugging
55      private static final boolean DEBUG = false;
56      //define shared variables for date/time
57      //define constants to be used in assigning default values for
58      //all date/time excluding duration
59      protected final static int YEAR = 2000;
60      protected final static int MONTH = 01;
61      protected final static int DAY = 01;
62      protected static final DatatypeFactory datatypeFactory = new DatatypeFactoryImpl();
63  
64      @Override
65      public short getAllowedFacets() {
66          return (XSSimpleTypeDecl.FACET_PATTERN | XSSimpleTypeDecl.FACET_WHITESPACE | XSSimpleTypeDecl.FACET_ENUMERATION | XSSimpleTypeDecl.FACET_MAXINCLUSIVE | XSSimpleTypeDecl.FACET_MININCLUSIVE | XSSimpleTypeDecl.FACET_MAXEXCLUSIVE | XSSimpleTypeDecl.FACET_MINEXCLUSIVE);
67      }//getAllowedFacets()
68  
69      // distinguishes between identity and equality for date/time values
70      // ie: two values representing the same "moment in time" but with different
71      // remembered timezones are now equal but not identical.
72      @Override
73      public boolean isIdentical(Object value1, Object value2) {
74          if (!(value1 instanceof DateTimeData) || !(value2 instanceof DateTimeData)) {
75              return false;
76          }
77  
78          DateTimeData v1 = (DateTimeData) value1;
79          DateTimeData v2 = (DateTimeData) value2;
80  
81          // original timezones must be the same in addition to date/time values
82          // being 'equal'
83          if ((v1.timezoneHr == v2.timezoneHr) && (v1.timezoneMin == v2.timezoneMin)) {
84              return v1.equals(v2);
85          }
86  
87          return false;
88      }//isIdentical()
89  
90      // the parameters are in compiled form (from getActualValue)
91      @Override
92      public int compare(Object value1, Object value2) {
93          return compareDates(((DateTimeData) value1),
94                  ((DateTimeData) value2), true);
95      }//compare()
96  
97      /**
98       * Compare algorithm described in dateDime (3.2.7). Duration datatype
99       * overwrites this method
100      *
101      * @param date1 normalized date representation of the first value
102      * @param date2 normalized date representation of the second value
103      * @param strict
104      * @return less, greater, less_equal, greater_equal, equal
105      */
106     protected short compareDates(DateTimeData date1, DateTimeData date2, boolean strict) {
107         if (date1.utc == date2.utc) {
108             return compareOrder(date1, date2);
109         }
110         short c1, c2;
111 
112         DateTimeData tempDate = new DateTimeData(null, this);
113 
114         if (date1.utc == 'Z') {
115 
116             //compare date1<=date1<=(date2 with time zone -14)
117             //
118             cloneDate(date2, tempDate); //clones date1 value to global temporary storage: fTempDate
119             tempDate.timezoneHr = 14;
120             tempDate.timezoneMin = 0;
121             tempDate.utc = '+';
122             normalize(tempDate);
123             c1 = compareOrder(date1, tempDate);
124             if (c1 == LESS_THAN) {
125                 return c1;
126             }
127 
128             //compare date1>=(date2 with time zone +14)
129             //
130             cloneDate(date2, tempDate); //clones date1 value to global temporary storage: tempDate
131             tempDate.timezoneHr = -14;
132             tempDate.timezoneMin = 0;
133             tempDate.utc = '-';
134             normalize(tempDate);
135             c2 = compareOrder(date1, tempDate);
136             if (c2 == GREATER_THAN) {
137                 return c2;
138             }
139 
140             return INDETERMINATE;
141         } else if (date2.utc == 'Z') {
142 
143             //compare (date1 with time zone -14)<=date2
144             //
145             cloneDate(date1, tempDate); //clones date1 value to global temporary storage: tempDate
146             tempDate.timezoneHr = -14;
147             tempDate.timezoneMin = 0;
148             tempDate.utc = '-';
149             if (DEBUG) {
150                 System.out.println("tempDate=" + dateToString(tempDate));
151             }
152             normalize(tempDate);
153             c1 = compareOrder(tempDate, date2);
154             if (DEBUG) {
155                 System.out.println("date=" + dateToString(date2));
156                 System.out.println("tempDate=" + dateToString(tempDate));
157             }
158             if (c1 == LESS_THAN) {
159                 return c1;
160             }
161 
162             //compare (date1 with time zone +14)<=date2
163             //
164             cloneDate(date1, tempDate); //clones date1 value to global temporary storage: tempDate
165             tempDate.timezoneHr = 14;
166             tempDate.timezoneMin = 0;
167             tempDate.utc = '+';
168             normalize(tempDate);
169             c2 = compareOrder(tempDate, date2);
170             if (DEBUG) {
171                 System.out.println("tempDate=" + dateToString(tempDate));
172             }
173             if (c2 == GREATER_THAN) {
174                 return c2;
175             }
176 
177             return INDETERMINATE;
178         }
179         return INDETERMINATE;
180 
181     }
182 
183     /**
184      * Given normalized values, determines order-relation between give date/time
185      * objects.
186      *
187      * @param date1 date/time object
188      * @param date2 date/time object
189      * @return 0 if date1 and date2 are equal, a value less than 0 if date1 is
190      * less than date2, a value greater than 0 if date1 is greater than date2
191      */
192     protected short compareOrder(DateTimeData date1, DateTimeData date2) {
193         if (date1.position < 1) {
194             if (date1.year < date2.year) {
195                 return -1;
196             }
197             if (date1.year > date2.year) {
198                 return 1;
199             }
200         }
201         if (date1.position < 2) {
202             if (date1.month < date2.month) {
203                 return -1;
204             }
205             if (date1.month > date2.month) {
206                 return 1;
207             }
208         }
209         if (date1.day < date2.day) {
210             return -1;
211         }
212         if (date1.day > date2.day) {
213             return 1;
214         }
215         if (date1.hour < date2.hour) {
216             return -1;
217         }
218         if (date1.hour > date2.hour) {
219             return 1;
220         }
221         if (date1.minute < date2.minute) {
222             return -1;
223         }
224         if (date1.minute > date2.minute) {
225             return 1;
226         }
227         if (date1.second < date2.second) {
228             return -1;
229         }
230         if (date1.second > date2.second) {
231             return 1;
232         }
233         if (date1.utc < date2.utc) {
234             return -1;
235         }
236         if (date1.utc > date2.utc) {
237             return 1;
238         }
239         return 0;
240     }
241 
242     /**
243      * Parses time hh:mm:ss.sss and time zone if any
244      *
245      * @param start
246      * @param end
247      * @param data
248      * @exception RuntimeException
249      */
250     protected void getTime(String buffer, int start, int end, DateTimeData data) throws RuntimeException {
251 
252         int stop = start + 2;
253 
254         //get hours (hh)
255         data.hour = parseInt(buffer, start, stop);
256 
257         //get minutes (mm)
258 
259         if (buffer.charAt(stop++) != ':') {
260             throw new RuntimeException("Error in parsing time zone");
261         }
262         start = stop;
263         stop = stop + 2;
264         data.minute = parseInt(buffer, start, stop);
265 
266         //get seconds (ss)
267         if (buffer.charAt(stop++) != ':') {
268             throw new RuntimeException("Error in parsing time zone");
269         }
270 
271         //find UTC sign if any
272         int sign = findUTCSign(buffer, start, end);
273 
274         //get seconds (ms)
275         start = stop;
276         stop = sign < 0 ? end : sign;
277         data.second = parseSecond(buffer, start, stop);
278 
279         //parse UTC time zone (hh:mm)
280         if (sign > 0) {
281             getTimeZone(buffer, data, sign, end);
282         }
283     }
284 
285     /**
286      * Parses date CCYY-MM-DD
287      *
288      * @param buffer
289      * @param start start position
290      * @param end end position
291      * @param date
292      * @exception RuntimeException
293      */
294     protected int getDate(String buffer, int start, int end, DateTimeData date) throws RuntimeException {
295 
296         start = getYearMonth(buffer, start, end, date);
297 
298         if (buffer.charAt(start++) != '-') {
299             throw new RuntimeException("CCYY-MM must be followed by '-' sign");
300         }
301         int stop = start + 2;
302         date.day = parseInt(buffer, start, stop);
303         return stop;
304     }
305 
306     /**
307      * Parses date CCYY-MM
308      *
309      * @param buffer
310      * @param start start position
311      * @param end end position
312      * @param date
313      * @exception RuntimeException
314      */
315     protected int getYearMonth(String buffer, int start, int end, DateTimeData date) throws RuntimeException {
316 
317         if (buffer.charAt(0) == '-') {
318             // REVISIT: date starts with preceding '-' sign
319             //          do we have to do anything with it?
320             //
321             start++;
322         }
323         int i = indexOf(buffer, start, end, '-');
324         if (i == -1) {
325             throw new RuntimeException("Year separator is missing or misplaced");
326         }
327         int length = i - start;
328         if (length < 4) {
329             throw new RuntimeException("Year must have 'CCYY' format");
330         } else if (length > 4 && buffer.charAt(start) == '0') {
331             throw new RuntimeException("Leading zeros are required if the year value would otherwise have fewer than four digits; otherwise they are forbidden");
332         }
333         date.year = parseIntYear(buffer, i);
334         if (buffer.charAt(i) != '-') {
335             throw new RuntimeException("CCYY must be followed by '-' sign");
336         }
337         start = ++i;
338         i = start + 2;
339         date.month = parseInt(buffer, start, i);
340         return i; //fStart points right after the MONTH
341     }
342 
343     /**
344      * Shared code from Date and YearMonth datatypes. Finds if time zone sign is
345      * present
346      *
347      * @param end
348      * @param date
349      * @exception RuntimeException
350      */
351     protected void parseTimeZone(String buffer, int start, int end, DateTimeData date) throws RuntimeException {
352 
353         //fStart points right after the date
354 
355         if (start < end) {
356             if (!isNextCharUTCSign(buffer, start, end)) {
357                 throw new RuntimeException("Error in month parsing");
358             } else {
359                 getTimeZone(buffer, date, start, end);
360             }
361         }
362     }
363 
364     /**
365      * Parses time zone: 'Z' or {+,-} followed by hh:mm
366      *
367      * @param data
368      * @param sign
369      * @exception RuntimeException
370      */
371     protected void getTimeZone(String buffer, DateTimeData data, int sign, int end) throws RuntimeException {
372         data.utc = buffer.charAt(sign);
373 
374         if (buffer.charAt(sign) == 'Z') {
375             if (end > (++sign)) {
376                 throw new RuntimeException("Error in parsing time zone");
377             }
378             return;
379         }
380         if (sign <= (end - 6)) {
381 
382             int negate = buffer.charAt(sign) == '-' ? -1 : 1;
383             //parse hr
384             int stop = ++sign + 2;
385             data.timezoneHr = negate * parseInt(buffer, sign, stop);
386             if (buffer.charAt(stop++) != ':') {
387                 throw new RuntimeException("Error in parsing time zone");
388             }
389 
390             //parse min
391             data.timezoneMin = negate * parseInt(buffer, stop, stop + 2);
392 
393             if (stop + 2 != end) {
394                 throw new RuntimeException("Error in parsing time zone");
395             }
396             if (data.timezoneHr != 0 || data.timezoneMin != 0) {
397                 data.normalized = false;
398             }
399         } else {
400             throw new RuntimeException("Error in parsing time zone");
401         }
402         if (DEBUG) {
403             System.out.println("time[hh]=" + data.timezoneHr + " time[mm]=" + data.timezoneMin);
404         }
405     }
406 
407     /**
408      * Computes index of given char within StringBuffer
409      *
410      * @param start
411      * @param end
412      * @param ch character to look for in StringBuffer
413      * @return index of ch within StringBuffer
414      */
415     protected int indexOf(String buffer, int start, int end, char ch) {
416         for (int i = start; i < end; i++) {
417             if (buffer.charAt(i) == ch) {
418                 return i;
419             }
420         }
421         return -1;
422     }
423 
424     /**
425      * Validates given date/time object accoring to W3C PR Schema [D.1 ISO 8601
426      * Conventions]
427      *
428      * @param data
429      */
430     protected void validateDateTime(DateTimeData data) {
431 
432         //REVISIT: should we throw an exception for not valid dates
433         //          or reporting an error message should be sufficient?
434 
435         /**
436          * XML Schema 1.1 - RQ-123: Allow year 0000 in date related types.
437          */
438         if (!Constants.SCHEMA_1_1_SUPPORT && data.year == 0) {
439             throw new RuntimeException("The year \"0000\" is an illegal year value");
440 
441         }
442 
443         if (data.month < 1 || data.month > 12) {
444             throw new RuntimeException("The month must have values 1 to 12");
445 
446         }
447 
448         //validate days
449         if (data.day > maxDayInMonthFor(data.year, data.month) || data.day < 1) {
450             throw new RuntimeException("The day must have values 1 to 31");
451         }
452 
453         //validate hours
454         if (data.hour > 23 || data.hour < 0) {
455             if (data.hour == 24 && data.minute == 0 && data.second == 0) {
456                 data.hour = 0;
457                 if (++data.day > maxDayInMonthFor(data.year, data.month)) {
458                     data.day = 1;
459                     if (++data.month > 12) {
460                         data.month = 1;
461                         if (Constants.SCHEMA_1_1_SUPPORT) {
462                             ++data.year;
463                         } else if (++data.year == 0) {
464                             data.year = 1;
465                         }
466                     }
467                 }
468             } else {
469                 throw new RuntimeException("Hour must have values 0-23, unless 24:00:00");
470             }
471         }
472 
473         //validate
474         if (data.minute > 59 || data.minute < 0) {
475             throw new RuntimeException("Minute must have values 0-59");
476         }
477 
478         //validate
479         if (data.second >= 60 || data.second < 0) {
480             throw new RuntimeException("Second must have values 0-59");
481 
482         }
483 
484         //validate
485         if (data.timezoneHr > 14 || data.timezoneHr < -14) {
486             throw new RuntimeException("Time zone should have range -14:00 to +14:00");
487         } else {
488             if ((data.timezoneHr == 14 || data.timezoneHr == -14) && data.timezoneMin != 0) {
489                 throw new RuntimeException("Time zone should have range -14:00 to +14:00");
490             } else if (data.timezoneMin > 59 || data.timezoneMin < -59) {
491                 throw new RuntimeException("Minute must have values 0-59");
492             }
493         }
494 
495     }
496 
497     /**
498      * Return index of UTC char: 'Z', '+', '-'
499      *
500      * @param start
501      * @param end
502      * @return index of the UTC character that was found
503      */
504     protected int findUTCSign(String buffer, int start, int end) {
505         int c;
506         for (int i = start; i < end; i++) {
507             c = buffer.charAt(i);
508             if (c == 'Z' || c == '+' || c == '-') {
509                 return i;
510             }
511 
512         }
513         return -1;
514     }
515 
516     /**
517      * Returns
518      * <code>true</code> if the character at start is 'Z', '+' or '-'.
519      */
520     protected final boolean isNextCharUTCSign(String buffer, int start, int end) {
521         if (start < end) {
522             char c = buffer.charAt(start);
523             return (c == 'Z' || c == '+' || c == '-');
524         }
525         return false;
526     }
527 
528     /**
529      * Given start and end position, parses string value
530      *
531      * @param buffer string to parse
532      * @param start start position
533      * @param end end position
534      * @return return integer representation of characters
535      */
536     protected int parseInt(String buffer, int start, int end)
537             throws NumberFormatException {
538         //REVISIT: more testing on this parsing needs to be done.
539         int radix = 10;
540         int result = 0;
541         int digit = 0;
542         int limit = -Integer.MAX_VALUE;
543         int multmin = limit / radix;
544         int i = start;
545         do {
546             digit = getDigit(buffer.charAt(i));
547             if (digit < 0) {
548                 throw new NumberFormatException("'" + buffer + "' has wrong format");
549             }
550             if (result < multmin) {
551                 throw new NumberFormatException("'" + buffer + "' has wrong format");
552             }
553             result *= radix;
554             if (result < limit + digit) {
555                 throw new NumberFormatException("'" + buffer + "' has wrong format");
556             }
557             result -= digit;
558 
559         } while (++i < end);
560         return -result;
561     }
562 
563     // parse Year differently to support negative value.
564     protected int parseIntYear(String buffer, int end) {
565         int radix = 10;
566         int result = 0;
567         boolean negative = false;
568         int i = 0;
569         int limit;
570         int multmin;
571         int digit = 0;
572 
573         if (buffer.charAt(0) == '-') {
574             negative = true;
575             limit = Integer.MIN_VALUE;
576             i++;
577 
578         } else {
579             limit = -Integer.MAX_VALUE;
580         }
581         multmin = limit / radix;
582         while (i < end) {
583             digit = getDigit(buffer.charAt(i++));
584             if (digit < 0) {
585                 throw new NumberFormatException("'" + buffer + "' has wrong format");
586             }
587             if (result < multmin) {
588                 throw new NumberFormatException("'" + buffer + "' has wrong format");
589             }
590             result *= radix;
591             if (result < limit + digit) {
592                 throw new NumberFormatException("'" + buffer + "' has wrong format");
593             }
594             result -= digit;
595         }
596 
597         if (negative) {
598             if (i > 1) {
599                 return result;
600             } else {
601                 throw new NumberFormatException("'" + buffer + "' has wrong format");
602             }
603         }
604         return -result;
605 
606     }
607 
608     /**
609      * If timezone present - normalize dateTime [E Adding durations to
610      * dateTimes]
611      *
612      * @param date CCYY-MM-DDThh:mm:ss+03
613      */
614     protected void normalize(DateTimeData date) {
615 
616         // REVISIT: we have common code in addDuration() for durations
617         //          should consider reorganizing it.
618         //
619 
620         //add minutes (from time zone)
621         int negate = -1;
622 
623         if (DEBUG) {
624             System.out.println("==>date.minute" + date.minute);
625             System.out.println("==>date.timezoneMin" + date.timezoneMin);
626         }
627         int temp = date.minute + negate * date.timezoneMin;
628         int carry = fQuotient(temp, 60);
629         date.minute = mod(temp, 60, carry);
630 
631         if (DEBUG) {
632             System.out.println("==>carry: " + carry);
633         }
634         //add hours
635         temp = date.hour + negate * date.timezoneHr + carry;
636         carry = fQuotient(temp, 24);
637         date.hour = mod(temp, 24, carry);
638         if (DEBUG) {
639             System.out.println("==>date.hour" + date.hour);
640             System.out.println("==>carry: " + carry);
641         }
642 
643         date.day = date.day + carry;
644 
645         while (true) {
646             temp = maxDayInMonthFor(date.year, date.month);
647             if (date.day < 1) {
648                 date.day = date.day + maxDayInMonthFor(date.year, date.month - 1);
649                 carry = -1;
650             } else if (date.day > temp) {
651                 date.day = date.day - temp;
652                 carry = 1;
653             } else {
654                 break;
655             }
656             temp = date.month + carry;
657             date.month = modulo(temp, 1, 13);
658             date.year = date.year + fQuotient(temp, 1, 13);
659             if (date.year == 0 && !Constants.SCHEMA_1_1_SUPPORT) {
660                 date.year = (date.timezoneHr < 0 || date.timezoneMin < 0) ? 1 : -1;
661             }
662         }
663         date.utc = 'Z';
664     }
665 
666     /**
667      * @param date
668      */
669     protected void saveUnnormalized(DateTimeData date) {
670         date.unNormYear = date.year;
671         date.unNormMonth = date.month;
672         date.unNormDay = date.day;
673         date.unNormHour = date.hour;
674         date.unNormMinute = date.minute;
675         date.unNormSecond = date.second;
676     }
677 
678     /**
679      * Resets object representation of date/time
680      *
681      * @param data date/time object
682      */
683     protected void resetDateObj(DateTimeData data) {
684         data.year = 0;
685         data.month = 0;
686         data.day = 0;
687         data.hour = 0;
688         data.minute = 0;
689         data.second = 0;
690         data.utc = 0;
691         data.timezoneHr = 0;
692         data.timezoneMin = 0;
693     }
694 
695     /**
696      * Given {year,month} computes maximum number of days for given month
697      *
698      * @param year
699      * @param month
700      * @return integer containg the number of days in a given month
701      */
702     protected int maxDayInMonthFor(int year, int month) {
703         //validate days
704         if (month == 4 || month == 6 || month == 9 || month == 11) {
705             return 30;
706         } else if (month == 2) {
707             if (isLeapYear(year)) {
708                 return 29;
709             } else {
710                 return 28;
711             }
712         } else {
713             return 31;
714         }
715     }
716 
717     private boolean isLeapYear(int year) {
718 
719         //REVISIT: should we take care about Julian calendar?
720         return ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)));
721     }
722 
723     //
724     // help function described in W3C PR Schema [E Adding durations to dateTimes]
725     //
726     protected int mod(int a, int b, int quotient) {
727         //modulo(a, b) = a - fQuotient(a,b)*b
728         return (a - quotient * b);
729     }
730 
731     //
732     // help function described in W3C PR Schema [E Adding durations to dateTimes]
733     //
734     protected int fQuotient(int a, int b) {
735 
736         //fQuotient(a, b) = the greatest integer less than or equal to a/b
737         return (int) Math.floor((float) a / b);
738     }
739 
740     //
741     // help function described in W3C PR Schema [E Adding durations to dateTimes]
742     //
743     protected int modulo(int temp, int low, int high) {
744         //modulo(a - low, high - low) + low
745         int a = temp - low;
746         int b = high - low;
747         return (mod(a, b, fQuotient(a, b)) + low);
748     }
749 
750     //
751     // help function described in W3C PR Schema [E Adding durations to dateTimes]
752     //
753     protected int fQuotient(int temp, int low, int high) {
754         //fQuotient(a - low, high - low)
755 
756         return fQuotient(temp - low, high - low);
757     }
758 
759     protected String dateToString(DateTimeData date) {
760         StringBuffer message = new StringBuffer(25);
761         append(message, date.year, 4);
762         message.append('-');
763         append(message, date.month, 2);
764         message.append('-');
765         append(message, date.day, 2);
766         message.append('T');
767         append(message, date.hour, 2);
768         message.append(':');
769         append(message, date.minute, 2);
770         message.append(':');
771         append(message, date.second);
772         append(message, (char) date.utc, 0);
773         return message.toString();
774     }
775 
776     protected final void append(StringBuffer message, int value, int nch) {
777         if (value == Integer.MIN_VALUE) {
778             message.append(value);
779             return;
780         }
781         if (value < 0) {
782             message.append('-');
783             value = -value;
784         }
785         if (nch == 4) {
786             if (value < 10) {
787                 message.append("000");
788             } else if (value < 100) {
789                 message.append("00");
790             } else if (value < 1000) {
791                 message.append('0');
792             }
793             message.append(value);
794         } else if (nch == 2) {
795             if (value < 10) {
796                 message.append('0');
797             }
798             message.append(value);
799         } else {
800             if (value != 0) {
801                 message.append((char) value);
802             }
803         }
804     }
805 
806     protected final void append(StringBuffer message, double value) {
807         if (value < 0) {
808             message.append('-');
809             value = -value;
810         }
811         if (value < 10) {
812             message.append('0');
813         }
814         append2(message, value);
815     }
816 
817     protected final void append2(StringBuffer message, double value) {
818         final int intValue = (int) value;
819         if (value == intValue) {
820             message.append(intValue);
821         } else {
822             append3(message, value);
823         }
824     }
825 
826     private void append3(StringBuffer message, double value) {
827         String d = String.valueOf(value);
828         int eIndex = d.indexOf('E');
829         if (eIndex == -1) {
830             message.append(d);
831             return;
832         }
833         int exp;
834         if (value < 1) {
835             // Need to convert from scientific notation of the form
836             // n.nnn...E-N (N >= 4) to a normal decimal value.
837             try {
838                 exp = parseInt(d, eIndex + 2, d.length());
839             } // This should never happen.
840             // It's only possible if String.valueOf(double) is broken.
841             catch (Exception e) {
842                 message.append(d);
843                 return;
844             }
845             message.append("0.");
846             for (int i = 1; i < exp; ++i) {
847                 message.append('0');
848             }
849             // Remove trailing zeros.
850             int end = eIndex - 1;
851             while (end > 0) {
852                 char c = d.charAt(end);
853                 if (c != '0') {
854                     break;
855                 }
856                 --end;
857             }
858             // Now append the digits to the end. Skip over the decimal point.
859             for (int i = 0; i <= end; ++i) {
860                 char c = d.charAt(i);
861                 if (c != '.') {
862                     message.append(c);
863                 }
864             }
865         } else {
866             // Need to convert from scientific notation of the form
867             // n.nnn...EN (N >= 7) to a normal decimal value.
868             try {
869                 exp = parseInt(d, eIndex + 1, d.length());
870             } // This should never happen.
871             // It's only possible if String.valueOf(double) is broken.
872             catch (Exception e) {
873                 message.append(d);
874                 return;
875             }
876             final int integerEnd = exp + 2;
877             for (int i = 0; i < eIndex; ++i) {
878                 char c = d.charAt(i);
879                 if (c != '.') {
880                     if (i == integerEnd) {
881                         message.append('.');
882                     }
883                     message.append(c);
884                 }
885             }
886             // Append trailing zeroes if necessary.
887             for (int i = integerEnd - eIndex; i > 0; --i) {
888                 message.append('0');
889             }
890         }
891     }
892 
893     protected double parseSecond(String buffer, int start, int end)
894             throws NumberFormatException {
895         int dot = -1;
896         for (int i = start; i < end; i++) {
897             char ch = buffer.charAt(i);
898             if (ch == '.') {
899                 dot = i;
900             } else if (ch > '9' || ch < '0') {
901                 throw new NumberFormatException("'" + buffer + "' has wrong format");
902             }
903         }
904         if (dot == -1) {
905             if (start + 2 != end) {
906                 throw new NumberFormatException("'" + buffer + "' has wrong format");
907             }
908         } else if (start + 2 != dot || dot + 1 == end) {
909             throw new NumberFormatException("'" + buffer + "' has wrong format");
910         }
911         return Double.parseDouble(buffer.substring(start, end));
912     }
913 
914     //
915     //Private help functions
916     //
917     private void cloneDate(DateTimeData finalValue, DateTimeData tempDate) {
918         tempDate.year = finalValue.year;
919         tempDate.month = finalValue.month;
920         tempDate.day = finalValue.day;
921         tempDate.hour = finalValue.hour;
922         tempDate.minute = finalValue.minute;
923         tempDate.second = finalValue.second;
924         tempDate.utc = finalValue.utc;
925         tempDate.timezoneHr = finalValue.timezoneHr;
926         tempDate.timezoneMin = finalValue.timezoneMin;
927     }
928 
929     /**
930      * Represents date time data
931      */
932     static final class DateTimeData implements XSDateTime {
933 
934         int year, month, day, hour, minute, utc;
935         double second;
936         int timezoneHr, timezoneMin;
937         private String originalValue;
938         boolean normalized = true;
939         int unNormYear;
940         int unNormMonth;
941         int unNormDay;
942         int unNormHour;
943         int unNormMinute;
944         double unNormSecond;
945         // used for comparisons - to decide the 'interesting' portions of
946         // a date/time based data type.
947         int position;
948         // a pointer to the type that was used go generate this data
949         // note that this is not the actual simple type, but one of the
950         // statically created XXXDV objects, so this won't cause any GC problem.
951         final AbstractDateTimeDV type;
952         private volatile String canonical;
953 
954         public DateTimeData(String originalValue, AbstractDateTimeDV type) {
955             this.originalValue = originalValue;
956             this.type = type;
957         }
958 
959         public DateTimeData(int year, int month, int day, int hour, int minute,
960                 double second, int utc, String originalValue, boolean normalized, AbstractDateTimeDV type) {
961             this.year = year;
962             this.month = month;
963             this.day = day;
964             this.hour = hour;
965             this.minute = minute;
966             this.second = second;
967             this.utc = utc;
968             this.type = type;
969             this.originalValue = originalValue;
970         }
971 
972         @Override
973         public boolean equals(Object obj) {
974             if (!(obj instanceof DateTimeData)) {
975                 return false;
976             }
977             return type.compareDates(this, (DateTimeData) obj, true) == 0;
978         }
979 
980         // If two DateTimeData are equals - then they should have the same
981         // hashcode. This means we need to convert the date to UTC before
982         // we return its hashcode.
983         // The DateTimeData is unfortunately mutable - so we cannot
984         // cache the result of the conversion...
985         //
986         @Override
987         public int hashCode() {
988             final DateTimeData tempDate = new DateTimeData(null, type);
989             type.cloneDate(this, tempDate);
990             type.normalize(tempDate);
991             return type.dateToString(tempDate).hashCode();
992         }
993 
994         @Override
995         public String toString() {
996             if (canonical == null) {
997                 canonical = type.dateToString(this);
998             }
999             return canonical;
1000         }
1001         /* (non-Javadoc)
1002          * @see org.apache.xerces.xs.datatypes.XSDateTime#getYear()
1003          */
1004 
1005         @Override
1006         public int getYears() {
1007             if (type instanceof DurationDV) {
1008                 return 0;
1009             }
1010             return normalized ? year : unNormYear;
1011         }
1012         /* (non-Javadoc)
1013          * @see org.apache.xerces.xs.datatypes.XSDateTime#getMonth()
1014          */
1015 
1016         @Override
1017         public int getMonths() {
1018             if (type instanceof DurationDV) {
1019                 return year * 12 + month;
1020             }
1021             return normalized ? month : unNormMonth;
1022         }
1023         /* (non-Javadoc)
1024          * @see org.apache.xerces.xs.datatypes.XSDateTime#getDay()
1025          */
1026 
1027         @Override
1028         public int getDays() {
1029             if (type instanceof DurationDV) {
1030                 return 0;
1031             }
1032             return normalized ? day : unNormDay;
1033         }
1034         /* (non-Javadoc)
1035          * @see org.apache.xerces.xs.datatypes.XSDateTime#getHour()
1036          */
1037 
1038         @Override
1039         public int getHours() {
1040             if (type instanceof DurationDV) {
1041                 return 0;
1042             }
1043             return normalized ? hour : unNormHour;
1044         }
1045         /* (non-Javadoc)
1046          * @see org.apache.xerces.xs.datatypes.XSDateTime#getMinutes()
1047          */
1048 
1049         @Override
1050         public int getMinutes() {
1051             if (type instanceof DurationDV) {
1052                 return 0;
1053             }
1054             return normalized ? minute : unNormMinute;
1055         }
1056         /* (non-Javadoc)
1057          * @see org.apache.xerces.xs.datatypes.XSDateTime#getSeconds()
1058          */
1059 
1060         @Override
1061         public double getSeconds() {
1062             if (type instanceof DurationDV) {
1063                 return day * 24 * 60 * 60 + hour * 60 * 60 + minute * 60 + second;
1064             }
1065             return normalized ? second : unNormSecond;
1066         }
1067         /* (non-Javadoc)
1068          * @see org.apache.xerces.xs.datatypes.XSDateTime#hasTimeZone()
1069          */
1070 
1071         @Override
1072         public boolean hasTimeZone() {
1073             return utc != 0;
1074         }
1075         /* (non-Javadoc)
1076          * @see org.apache.xerces.xs.datatypes.XSDateTime#getTimeZoneHours()
1077          */
1078 
1079         @Override
1080         public int getTimeZoneHours() {
1081             return timezoneHr;
1082         }
1083         /* (non-Javadoc)
1084          * @see org.apache.xerces.xs.datatypes.XSDateTime#getTimeZoneMinutes()
1085          */
1086 
1087         @Override
1088         public int getTimeZoneMinutes() {
1089             return timezoneMin;
1090         }
1091         /* (non-Javadoc)
1092          * @see org.apache.xerces.xs.datatypes.XSDateTime#getLexicalValue()
1093          */
1094 
1095         @Override
1096         public String getLexicalValue() {
1097             return originalValue;
1098         }
1099         /* (non-Javadoc)
1100          * @see org.apache.xerces.xs.datatypes.XSDateTime#normalize()
1101          */
1102 
1103         @Override
1104         public XSDateTime normalize() {
1105             if (!normalized) {
1106                 DateTimeData dt = (DateTimeData) this.clone();
1107                 dt.normalized = true;
1108                 return dt;
1109             }
1110             return this;
1111         }
1112         /* (non-Javadoc)
1113          * @see org.apache.xerces.xs.datatypes.XSDateTime#isNormalized()
1114          */
1115 
1116         @Override
1117         public boolean isNormalized() {
1118             return normalized;
1119         }
1120 
1121         @Override
1122         public Object clone() {
1123             DateTimeData dt = new DateTimeData(this.year, this.month, this.day, this.hour,
1124                     this.minute, this.second, this.utc, this.originalValue, this.normalized, this.type);
1125             dt.canonical = this.canonical;
1126             dt.position = position;
1127             dt.timezoneHr = this.timezoneHr;
1128             dt.timezoneMin = this.timezoneMin;
1129             dt.unNormYear = this.unNormYear;
1130             dt.unNormMonth = this.unNormMonth;
1131             dt.unNormDay = this.unNormDay;
1132             dt.unNormHour = this.unNormHour;
1133             dt.unNormMinute = this.unNormMinute;
1134             dt.unNormSecond = this.unNormSecond;
1135             return dt;
1136         }
1137 
1138         /* (non-Javadoc)
1139          * @see org.apache.xerces.xs.datatypes.XSDateTime#getXMLGregorianCalendar()
1140          */
1141         @Override
1142         public XMLGregorianCalendar getXMLGregorianCalendar() {
1143             return type.getXMLGregorianCalendar(this);
1144         }
1145         /* (non-Javadoc)
1146          * @see org.apache.xerces.xs.datatypes.XSDateTime#getDuration()
1147          */
1148 
1149         @Override
1150         public Duration getDuration() {
1151             return type.getDuration(this);
1152         }
1153     }
1154 
1155     protected XMLGregorianCalendar getXMLGregorianCalendar(DateTimeData data) {
1156         return null;
1157     }
1158 
1159     protected Duration getDuration(DateTimeData data) {
1160         return null;
1161     }
1162 
1163     protected final BigDecimal getFractionalSecondsAsBigDecimal(DateTimeData data) {
1164         final StringBuffer buf = new StringBuffer();
1165         append3(buf, data.unNormSecond);
1166         String value = buf.toString();
1167         final int index = value.indexOf('.');
1168         if (index == -1) {
1169             return null;
1170         }
1171         value = value.substring(index);
1172         final BigDecimal _val = new BigDecimal(value);
1173         if (_val.compareTo(BigDecimal.valueOf(0)) == 0) {
1174             return null;
1175         }
1176         return _val;
1177     }
1178 }