View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    https://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.api.util;
21  
22  import static org.apache.directory.api.util.TimeZones.GMT;
23  
24  import java.text.ParseException;
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.GregorianCalendar;
28  import java.util.Locale;
29  import java.util.TimeZone;
30  
31  import org.apache.directory.api.i18n.I18n;
32  
33  
34  /**
35   * <p>This class represents the generalized time syntax as defined in 
36   * RFC 4517 section 3.3.13.</p>
37   * 
38   * <p>The date, time and time zone information is internally backed
39   * by an {@link java.util.Calendar} object</p>
40   * 
41   * <p>Leap seconds are not supported, as {@link java.util.Calendar}
42   * does not support leap seconds.</p>
43   * 
44   * <pre>
45   * 3.3.13.  Generalized Time
46   *
47   *  A value of the Generalized Time syntax is a character string
48   *  representing a date and time.  The LDAP-specific encoding of a value
49   *  of this syntax is a restriction of the format defined in [ISO8601],
50   *  and is described by the following ABNF:
51   *
52   *     GeneralizedTime = century year month day hour
53   *                          [ minute [ second / leap-second ] ]
54   *                          [ fraction ]
55   *                          g-time-zone
56   *
57   *     century = 2(%x30-39) ; "00" to "99"
58   *     year    = 2(%x30-39) ; "00" to "99"
59   *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
60   *               / ( %x31 %x30-32 ) ; "10" to "12"
61   *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
62   *               / ( %x31-32 %x30-39 ) ; "10" to "29"
63   *               / ( %x33 %x30-31 )    ; "30" to "31"
64   *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
65   *     minute  = %x30-35 %x30-39                        ; "00" to "59"
66   *
67   *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
68   *     leap-second = ( %x36 %x30 )       ; "60"
69   *
70   *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
71   *     g-time-zone     = %x5A  ; "Z"
72   *                       / g-differential
73   *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
74   *     MINUS           = %x2D  ; minus sign ("-")
75   *
76   *  The &lt;DOT&gt;, &lt;COMMA&gt;, and &lt;PLUS&gt; rules are defined in [RFC4512].
77   *
78   *  The above ABNF allows character strings that do not represent valid
79   *  dates (in the Gregorian calendar) and/or valid times (e.g., February
80   *  31, 1994).  Such character strings SHOULD be considered invalid for
81   *  this syntax.
82   * <br>
83   *  The time value represents coordinated universal time (equivalent to
84   *  Greenwich Mean Time) if the "Z" form of &lt;g-time-zone&gt; is used;
85   *  otherwise, the value represents a local time in the time zone
86   *  indicated by &lt;g-differential&gt;.  In the latter case, coordinated
87   *  universal time can be calculated by subtracting the differential from
88   *  the local time.  The "Z" form of &lt;g-time-zone&gt; SHOULD be used in
89   *  preference to &lt;g-differential&gt;.
90   *  <br>
91   *  If &lt;minute&gt; is omitted, then &lt;fraction&gt; represents a fraction of an
92   *  hour; otherwise, if &lt;second&gt; and &lt;leap-second&gt; are omitted, then
93   *  &lt;fraction&gt; represents a fraction of a minute; otherwise, &lt;fraction&gt;
94   *  represents a fraction of a second.
95   *
96   *     Examples:
97   *        199412161032Z
98   *        199412160532-0500
99   *  
100  *  Both example values represent the same coordinated universal time:
101  *  10:32 AM, December 16, 1994.
102  *  <br>
103  *  The LDAP definition for the Generalized Time syntax is:
104  *  
105  *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
106  *  
107  *  This syntax corresponds to the GeneralizedTime ASN.1 type from
108  *  [ASN.1], with the constraint that local time without a differential
109  *  SHALL NOT be used.
110  * </pre>
111  */
112 public class GeneralizedTime implements Comparable<GeneralizedTime>
113 {
114     /** A Date far in the future, when Micro$oft would have vanished for a long time... */
115     private static final Date INFINITE = new Date( 0x7FFFFFFFFFFFFFFFL );
116 
117     /**
118      * The format of the generalized time.
119      */
120     public enum Format
121     {
122         /** Time format with minutes and seconds, excluding fraction. */
123         YEAR_MONTH_DAY_HOUR_MIN_SEC,
124         
125         /** Time format with minutes and seconds, including fraction. */
126         YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
127 
128         /** Time format with minutes, seconds are omitted, excluding fraction. */
129         YEAR_MONTH_DAY_HOUR_MIN,
130         
131         /** Time format with minutes seconds are omitted, including fraction of a minute. */
132         YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
133 
134         /** Time format, minutes and seconds are omitted, excluding fraction. */
135         YEAR_MONTH_DAY_HOUR,
136         
137         /** Time format, minutes and seconds are omitted, including fraction of an hour. */
138         YEAR_MONTH_DAY_HOUR_FRACTION;
139     }
140 
141     /**
142      * The fraction delimiter of the generalized time.
143      */
144     public enum FractionDelimiter
145     {
146         /** Use a dot as fraction delimiter. */
147         DOT,
148         /** Use a comma as fraction delimiter. */
149         COMMA
150     }
151 
152     /**
153      * The time zone format of the generalized time.
154      */
155     public enum TimeZoneFormat
156     {
157         /** g-time-zone (Zulu) format. */
158         Z,
159         /** g-differential format, using hour only. */
160         DIFF_HOUR,
161         /** g-differential format, using hour and minute. */
162         DIFF_HOUR_MINUTE
163     }
164 
165     /** The user provided value */
166     private String upGeneralizedTime;
167 
168     /** The user provided format */
169     private Format upFormat;
170 
171     /** The user provided time zone format */
172     private TimeZoneFormat upTimeZoneFormat;
173 
174     /** The user provided fraction delimiter */
175     private FractionDelimiter upFractionDelimiter;
176 
177     /** the user provided fraction length */
178     private int upFractionLength;
179 
180     /** The calendar */
181     private Calendar calendar;
182 
183 
184     /**
185      * 
186      * Creates a new instance of GeneralizedTime by setting the date to an instance of Calendar.
187      * @see #GeneralizedTime(Calendar)
188      * 
189      * @param date the date
190      */
191     public GeneralizedTime( Date date )
192     {
193         calendar = new GregorianCalendar( GMT, Locale.ROOT );
194         calendar.setTime( date );
195         setUp( calendar );
196     }
197 
198 
199     /**
200      * Creates a new instance of GeneralizedTime, based on the given Calendar object.
201      * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
202      * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
203      *
204      * @param calendar the calendar containing the date, time and timezone information
205      */
206     public GeneralizedTime( Calendar calendar )
207     {
208         setUp( calendar );
209     }
210     
211     
212     /**
213      * Creates a new instance of GeneralizedTime by setting the time to an instance of Calendar.
214      * @see #GeneralizedTime(Calendar)
215      * 
216      * @param timeInMillis the time in milliseconds
217      */
218     public GeneralizedTime( long timeInMillis )
219     {
220         Locale locale = Locale.getDefault();
221         TimeZone timeZone = TimeZone.getTimeZone( "GMT"  );
222         calendar = Calendar.getInstance( timeZone, locale );
223         calendar.setTimeInMillis( timeInMillis );
224         setUp( calendar );
225     }
226 
227 
228     /**
229      * Creates a new instance of GeneralizedTime, based on the
230      * given generalized time string.
231      *
232      * @param generalizedTime the generalized time
233      * 
234      * @throws ParseException if the given generalized time can't be parsed.
235      */
236     public GeneralizedTime( String generalizedTime ) throws ParseException
237     {
238         if ( generalizedTime == null )
239         {
240             throw new ParseException( I18n.err( I18n.ERR_17043_GENERALIZED_TIME_NULL ), 0 );
241         }
242 
243         this.upGeneralizedTime = generalizedTime;
244 
245         calendar = new GregorianCalendar( GMT, Locale.ROOT );
246         calendar.setTimeInMillis( 0 );
247         calendar.setLenient( false );
248 
249         parseYear();
250         parseMonth();
251         parseDay();
252         parseHour();
253 
254         if ( upGeneralizedTime.length() < 11 )
255         {
256             throw new ParseException( I18n.err( I18n.ERR_17044_BAD_GENERALIZED_TIME ), 10 );
257         }
258 
259         // pos 10: 
260         // if digit => minute field
261         // if . or , => fraction of hour field
262         // if Z or + or - => timezone field
263         // else error
264         int pos = 10;
265         char c = upGeneralizedTime.charAt( pos );
266         
267         if ( ( '0' <= c ) && ( c <= '9' ) )
268         {
269             parseMinute();
270 
271             if ( upGeneralizedTime.length() < 13 )
272             {
273                 throw new ParseException( I18n.err( I18n.ERR_17045_BAD_GENERALIZED_TIME ), 12 );
274             }
275 
276             // pos 12: 
277             // if digit => second field
278             // if . or , => fraction of minute field
279             // if Z or + or - => timezone field
280             // else error
281             pos = 12;
282             c = upGeneralizedTime.charAt( pos );
283             
284             if ( ( '0' <= c ) && ( c <= '9' ) )
285             {
286                 parseSecond();
287 
288                 if ( upGeneralizedTime.length() < 15 )
289                 {
290                     throw new ParseException( I18n.err( I18n.ERR_17046_BAD_GENERALIZED_TIME ), 14 );
291                 }
292 
293                 // pos 14: 
294                 // if . or , => fraction of second field
295                 // if Z or + or - => timezone field
296                 // else error
297                 pos = 14;
298                 c = upGeneralizedTime.charAt( pos );
299                 
300                 if ( ( c == '.' ) || ( c == ',' ) )
301                 {
302                     // read fraction of second
303                     parseFractionOfSecond();
304                     pos += 1 + upFractionLength;
305 
306                     parseTimezone( pos );
307                     upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
308                 }
309                 else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
310                 {
311                     // read timezone
312                     parseTimezone( pos );
313                     upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
314                 }
315                 else
316                 {
317                     throw new ParseException( I18n.err( I18n.ERR_17047_TIME_TOO_SHORT ), 14 );
318                 }
319             }
320             else if ( ( c == '.' ) || ( c == ',' ) )
321             {
322                 // read fraction of minute
323                 parseFractionOfMinute();
324                 pos += 1 + upFractionLength;
325 
326                 parseTimezone( pos );
327                 upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
328             }
329             else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
330             {
331                 // read timezone
332                 parseTimezone( pos );
333                 upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
334             }
335             else
336             {
337                 throw new ParseException( I18n.err( I18n.ERR_17048_TIME_TOO_SHORT ), 12 );
338             }
339         }
340         else if ( ( c == '.' ) || ( c == ',' ) )
341         {
342             // read fraction of hour
343             parseFractionOfHour();
344             pos += 1 + upFractionLength;
345 
346             parseTimezone( pos );
347             upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
348         }
349         else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
350         {
351             // read timezone
352             parseTimezone( pos );
353             upFormat = Format.YEAR_MONTH_DAY_HOUR;
354         }
355         else
356         {
357             throw new ParseException( I18n.err( I18n.ERR_17049_INVALID_GENERALIZED_TIME ), 10 );
358         }
359 
360         // this calculates and verifies the calendar
361         /* Not sure we should do that... */
362         try
363         {
364             calendar.getTimeInMillis();
365         }
366         catch ( IllegalArgumentException iae )
367         {
368             throw new ParseException( I18n.err( I18n.ERR_17050_INVALID_DATE_TIME ), 0 );
369         }
370 
371         calendar.setLenient( true );
372     }
373 
374 
375     private void setUp( Calendar newCalendar )
376     {
377         if ( newCalendar == null )
378         {
379             throw new IllegalArgumentException( I18n.err( I18n.ERR_17051_CALENDAR_NULL ) );
380         }
381 
382         this.calendar = newCalendar;
383         upGeneralizedTime = null;
384         upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
385         upTimeZoneFormat = TimeZoneFormat.Z;
386         upFractionDelimiter = FractionDelimiter.DOT;
387         upFractionLength = 3;
388     }
389 
390 
391     private void parseTimezone( int pos ) throws ParseException
392     {
393         if ( upGeneralizedTime.length() < pos + 1 )
394         {
395             throw new ParseException( I18n.err( I18n.ERR_17052_TIME_TOO_SHOR_NO_TZ ), pos );
396         }
397 
398         char c = upGeneralizedTime.charAt( pos );
399         
400         if ( c == 'Z' )
401         {
402             calendar.setTimeZone( GMT );
403             upTimeZoneFormat = TimeZoneFormat.Z;
404 
405             if ( upGeneralizedTime.length() > pos + 1 )
406             {
407                 throw new ParseException( I18n.err( I18n.ERR_17053_MISSING_TZ ), pos + 1 );
408             }
409         }
410         else if ( ( c == '+' ) || ( c == '-' ) )
411         {
412             StringBuilder sb = new StringBuilder( "GMT" );
413             sb.append( c );
414 
415             String digits = getAllDigits( pos + 1 );
416             sb.append( digits );
417 
418             if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
419             {
420                 TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
421                 calendar.setTimeZone( timeZone );
422                 upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
423             }
424             else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
425             {
426                 TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
427                 calendar.setTimeZone( timeZone );
428                 upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
429             }
430             else
431             {
432                 throw new ParseException( I18n.err( I18n.ERR_17054_TZ_MUST_BE_2_OR_4_DIGITS ), pos );
433             }
434 
435             if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
436             {
437                 throw new ParseException( I18n.err( I18n.ERR_17053_MISSING_TZ ), pos + 1 + digits.length() );
438             }
439         }
440     }
441 
442 
443     private void parseFractionOfSecond() throws ParseException
444     {
445         parseFractionDelmiter( 14 );
446         String fraction = getFraction( 14 + 1 );
447         upFractionLength = fraction.length();
448 
449         double fract = Double.parseDouble( "0." + fraction );
450         int millisecond = ( int ) Math.floor( fract * 1000 );
451 
452         calendar.set( GregorianCalendar.MILLISECOND, millisecond );
453     }
454 
455 
456     private void parseFractionOfMinute() throws ParseException
457     {
458         parseFractionDelmiter( 12 );
459         String fraction = getFraction( 12 + 1 );
460         upFractionLength = fraction.length();
461 
462         double fract = Double.parseDouble( "0." + fraction );
463         int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
464         int second = milliseconds / 1000;
465         int millisecond = milliseconds - ( second * 1000 );
466 
467         calendar.set( Calendar.SECOND, second );
468         calendar.set( Calendar.MILLISECOND, millisecond );
469     }
470 
471 
472     private void parseFractionOfHour() throws ParseException
473     {
474         parseFractionDelmiter( 10 );
475         String fraction = getFraction( 10 + 1 );
476         upFractionLength = fraction.length();
477 
478         double fract = Double.parseDouble( "0." + fraction );
479         int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
480         int minute = milliseconds / ( 1000 * 60 );
481         int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
482         int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
483 
484         calendar.set( Calendar.MINUTE, minute );
485         calendar.set( Calendar.SECOND, second );
486         calendar.set( Calendar.MILLISECOND, millisecond );
487     }
488 
489 
490     private void parseFractionDelmiter( int fractionDelimiterPos )
491     {
492         char c = upGeneralizedTime.charAt( fractionDelimiterPos );
493         upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
494     }
495 
496 
497     private String getFraction( int startIndex ) throws ParseException
498     {
499         String fraction = getAllDigits( startIndex );
500 
501         // minimum one digit
502         if ( fraction.length() == 0 )
503         {
504             throw new ParseException( I18n.err( I18n.ERR_17055_MISSING_FRACTION ), startIndex );
505         }
506 
507         return fraction;
508     }
509 
510 
511     private String getAllDigits( int startIndex )
512     {
513         StringBuilder sb = new StringBuilder();
514         while ( upGeneralizedTime.length() > startIndex )
515         {
516             char c = upGeneralizedTime.charAt( startIndex );
517             if ( '0' <= c && c <= '9' )
518             {
519                 sb.append( c );
520                 startIndex++;
521             }
522             else
523             {
524                 break;
525             }
526         }
527         return sb.toString();
528     }
529 
530 
531     private void parseSecond() throws ParseException
532     {
533         // read minute
534         if ( upGeneralizedTime.length() < 14 )
535         {
536             throw new ParseException( I18n.err( I18n.ERR_17056_TIME_TOO_SHORT_NO_SECOND ), 12 );
537         }
538         try
539         {
540             int second = Strings.parseInt( upGeneralizedTime.substring( 12, 14 ) );
541             calendar.set( Calendar.SECOND, second );
542         }
543         catch ( NumberFormatException e )
544         {
545             throw new ParseException( I18n.err( I18n.ERR_17057_SECOND_NOT_NUM ), 12 );
546         }
547     }
548 
549 
550     private void parseMinute() throws ParseException
551     {
552         // read minute
553         if ( upGeneralizedTime.length() < 12 )
554         {
555             throw new ParseException( I18n.err( I18n.ERR_17058_MISSING_MINUTE ), 10 );
556         }
557         try
558         {
559             int minute = Strings.parseInt( upGeneralizedTime.substring( 10, 12 ) );
560             calendar.set( Calendar.MINUTE, minute );
561         }
562         catch ( NumberFormatException e )
563         {
564             throw new ParseException( I18n.err( I18n.ERR_17059_MIN_NOT_NUM ), 10 );
565         }
566     }
567 
568 
569     private void parseHour() throws ParseException
570     {
571         if ( upGeneralizedTime.length() < 10 )
572         {
573             throw new ParseException( I18n.err( I18n.ERR_17060_TIME_TO_SHORT_MISSING_HOUR ), 8 );
574         }
575         try
576         {
577             int hour = Strings.parseInt( upGeneralizedTime.substring( 8, 10 ) );
578             calendar.set( Calendar.HOUR_OF_DAY, hour );
579         }
580         catch ( NumberFormatException e )
581         {
582             throw new ParseException( I18n.err( I18n.ERR_17061_HOUR_NOT_NUM ), 8 );
583         }
584     }
585 
586 
587     private void parseDay() throws ParseException
588     {
589         if ( upGeneralizedTime.length() < 8 )
590         {
591             throw new ParseException( I18n.err( I18n.ERR_17062_TIME_TO_SHORT_MISSING_DAY ), 6 );
592         }
593         try
594         {
595             int day = Strings.parseInt( upGeneralizedTime.substring( 6, 8 ) );
596             calendar.set( Calendar.DAY_OF_MONTH, day );
597         }
598         catch ( NumberFormatException e )
599         {
600             throw new ParseException( I18n.err( I18n.ERR_17063_DAY_NOT_NUM ), 6 );
601         }
602     }
603 
604 
605     private void parseMonth() throws ParseException
606     {
607         if ( upGeneralizedTime.length() < 6 )
608         {
609             throw new ParseException( I18n.err( I18n.ERR_17064_TIME_TO_SHORT_MISSING_MONTH ), 4 );
610         }
611         try
612         {
613             int month = Strings.parseInt( upGeneralizedTime.substring( 4, 6 ) );
614             calendar.set( Calendar.MONTH, month - 1 );
615         }
616         catch ( NumberFormatException e )
617         {
618             throw new ParseException( I18n.err( I18n.ERR_17065_MONTH_NOT_NUM ), 4 );
619         }
620     }
621 
622 
623     private void parseYear() throws ParseException
624     {
625         if ( upGeneralizedTime.length() < 4 )
626         {
627             throw new ParseException( I18n.err( I18n.ERR_17066_TIME_TO_SHORT_MISSING_YEAR ), 0 );
628         }
629         try
630         {
631             int year = Strings.parseInt( upGeneralizedTime.substring( 0, 4 ) );
632             calendar.set( Calendar.YEAR, year );
633         }
634         catch ( NumberFormatException e )
635         {
636             throw new ParseException( I18n.err( I18n.ERR_17067_YEAR_NOT_NUM ), 0 );
637         }
638     }
639 
640 
641     /**
642      * Returns the string representation of this generalized time. 
643      * This method uses the same format as the user provided format.
644      *
645      * @return the string representation of this generalized time
646      */
647     public String toGeneralizedTime()
648     {
649         return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
650     }
651 
652 
653     /**
654      * Returns the string representation of this generalized time. 
655      * This method uses the same format as the user provided format.
656      *
657      * @return the string representation of this generalized time
658      */
659     public String toGeneralizedTimeWithoutFraction()
660     {
661         return toGeneralizedTime( getFormatWithoutFraction( upFormat ), upFractionDelimiter, upFractionLength,
662             upTimeZoneFormat );
663     }
664 
665 
666     /**
667      * Gets the corresponding format with fraction.
668      *
669      * @param f the format
670      * @return the corresponding format without fraction
671      */
672     private Format getFormatWithoutFraction( Format f )
673     {
674         switch ( f )
675         {
676             case YEAR_MONTH_DAY_HOUR_FRACTION:
677                 return Format.YEAR_MONTH_DAY_HOUR;
678             case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
679                 return Format.YEAR_MONTH_DAY_HOUR_MIN;
680             case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
681                 return Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
682             default:
683                 break;
684         }
685 
686         return f;
687     }
688 
689 
690     /**
691      * Returns the string representation of this generalized time.
692      * 
693      * @param format the target format
694      * @param fractionDelimiter the target fraction delimiter, may be null
695      * @param fractionLength the fraction length
696      * @param timeZoneFormat the target time zone format
697      * 
698      * @return the string
699      */
700     public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
701         TimeZoneFormat timeZoneFormat )
702     {
703         Calendar clonedCalendar = ( Calendar ) calendar.clone();
704 
705         if ( timeZoneFormat == TimeZoneFormat.Z )
706         {
707             clonedCalendar.setTimeZone( GMT );
708         }
709 
710         // Create the result. It can contain a maximum of 23 chars
711         byte[] result = new byte[23];
712 
713         // The starting point
714         int pos = 0;
715 
716         // Inject the year
717         int year = clonedCalendar.get( Calendar.YEAR );
718 
719         result[pos++] = ( byte ) ( ( year / 1000 ) + '0' );
720         year %= 1000;
721 
722         result[pos++] = ( byte ) ( ( year / 100 ) + '0' );
723         year %= 100;
724 
725         result[pos++] = ( byte ) ( ( year / 10 ) + '0' );
726 
727         result[pos++] = ( byte ) ( ( year % 10 ) + '0' );
728 
729         // Inject the month
730         int month = clonedCalendar.get( Calendar.MONTH ) + 1;
731 
732         result[pos++] = ( byte ) ( ( month / 10 ) + '0' );
733 
734         result[pos++] = ( byte ) ( ( month % 10 ) + '0' );
735 
736         // Inject the day
737         int day = clonedCalendar.get( Calendar.DAY_OF_MONTH );
738 
739         result[pos++] = ( byte ) ( ( day / 10 ) + '0' );
740 
741         result[pos++] = ( byte ) ( ( day % 10 ) + '0' );
742 
743         // Inject the hour
744         int hour = clonedCalendar.get( Calendar.HOUR_OF_DAY );
745 
746         result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
747 
748         result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
749 
750         switch ( format )
751         {
752             case YEAR_MONTH_DAY_HOUR_MIN_SEC:
753                 // Inject the minutes
754                 int minute = clonedCalendar.get( Calendar.MINUTE );
755 
756                 result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
757 
758                 result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
759 
760                 // Inject the seconds
761                 int second = clonedCalendar.get( Calendar.SECOND );
762 
763                 result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
764 
765                 result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
766 
767                 break;
768 
769             case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
770                 // Inject the minutes
771                 minute = clonedCalendar.get( Calendar.MINUTE );
772 
773                 result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
774 
775                 result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
776 
777                 // Inject the seconds
778                 second = clonedCalendar.get( Calendar.SECOND );
779 
780                 result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
781 
782                 result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
783 
784                 // Inject the fraction
785                 if ( fractionDelimiter == FractionDelimiter.COMMA )
786                 {
787                     result[pos++] = ',';
788                 }
789                 else
790                 {
791                     result[pos++] = '.';
792                 }
793 
794                 // Inject the fraction
795                 int millisecond = clonedCalendar.get( Calendar.MILLISECOND );
796 
797                 result[pos++] = ( byte ) ( ( millisecond / 100 ) + '0' );
798                 millisecond %= 100;
799 
800                 result[pos++] = ( byte ) ( ( millisecond / 10 ) + '0' );
801 
802                 //if ( millisecond > 0 )
803                 result[pos++] = ( byte ) ( ( millisecond % 10 ) + '0' );
804 
805                 break;
806 
807             case YEAR_MONTH_DAY_HOUR_MIN:
808                 // Inject the minutes
809                 minute = clonedCalendar.get( Calendar.MINUTE );
810 
811                 result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
812 
813                 result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
814                 break;
815 
816             case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
817                 // Inject the minutes
818                 minute = clonedCalendar.get( Calendar.MINUTE );
819 
820                 result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
821 
822                 result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
823 
824                 // sec + millis => fraction of a minute
825                 int fraction = 1000 * clonedCalendar.get( Calendar.SECOND )
826                     + clonedCalendar.get( Calendar.MILLISECOND );
827                 fraction /= 60;
828 
829                 if ( fraction > 0 )
830                 {
831                     if ( fractionDelimiter == FractionDelimiter.COMMA )
832                     {
833                         result[pos++] = ',';
834                     }
835                     else
836                     {
837                         result[pos++] = '.';
838                     }
839 
840                     // At this point, the fraction should be in [999, 1]
841                     result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
842                     fraction %= 100;
843 
844                     if ( fraction > 0 )
845                     {
846                         result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
847 
848                         if ( fraction > 0 )
849                         {
850                             result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
851                         }
852                     }
853                 }
854 
855                 break;
856 
857             case YEAR_MONTH_DAY_HOUR:
858                 // nothing to add
859                 break;
860 
861             case YEAR_MONTH_DAY_HOUR_FRACTION:
862                 // min + sec + millis => fraction of an hour
863                 fraction = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000
864                     * clonedCalendar.get( Calendar.SECOND )
865                     + clonedCalendar.get( Calendar.MILLISECOND );
866                 fraction /= 60 * 60;
867 
868                 // At this point, the fraction should be in [999, 1]
869                 if ( fraction > 0 )
870                 {
871                     if ( fractionDelimiter == FractionDelimiter.COMMA )
872                     {
873                         result[pos++] = ',';
874                     }
875                     else
876                     {
877                         result[pos++] = '.';
878                     }
879 
880                     result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
881                     fraction %= 100;
882 
883                     if ( fraction > 0 )
884                     {
885                         result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
886 
887                         if ( fraction > 0 )
888                         {
889                             result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
890                         }
891                     }
892                 }
893 
894                 break;
895 
896             default:
897                 throw new IllegalArgumentException( I18n.err( I18n.ERR_17069_UNEXPECTED_FORMAT, format ) );
898         }
899 
900         if ( ( timeZoneFormat == TimeZoneFormat.Z ) && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
901         {
902             result[pos++] = 'Z';
903         }
904         else
905         {
906             // g-differential
907             int offset = clonedCalendar.get( Calendar.ZONE_OFFSET ) + clonedCalendar.get( Calendar.DST_OFFSET );
908             
909             
910             if ( offset < 0 )
911             {
912                 result[pos++] = '-';
913             }
914             else
915             {
916                 result[pos++] = '+';
917             }
918 
919             offset = Math.abs( offset );
920             hour = offset / ( 60 * 60 * 1000 );
921             int minute = ( offset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
922 
923             // The offset hour
924             result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
925 
926             result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
927 
928             if ( ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE ) || ( timeZoneFormat == TimeZoneFormat.Z ) )
929             {
930                 // The offset minute
931                 result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
932 
933                 result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
934             }
935         }
936 
937         return Strings.utf8ToString( result, 0, pos );
938     }
939 
940 
941     /**
942      * Gets the calendar. It could be used to manipulate this 
943      * {@link GeneralizedTime} settings.
944      * 
945      * @return the calendar
946      */
947     public Calendar getCalendar()
948     {
949         return calendar;
950     }
951 
952 
953     /**
954      * {@inheritDoc}
955      */
956     @Override
957     public String toString()
958     {
959         return toGeneralizedTime();
960     }
961 
962 
963     /**
964      * {@inheritDoc}
965      */
966     @Override
967     public int hashCode()
968     {
969         final int prime = 31;
970         int result = 1;
971         result = prime * result + calendar.hashCode();
972         return result;
973     }
974 
975 
976     /**
977      * {@inheritDoc}
978      */
979     @Override
980     public boolean equals( Object obj )
981     {
982         if ( obj instanceof GeneralizedTime )
983         {
984             GeneralizedTime other = ( GeneralizedTime ) obj;
985             return calendar.equals( other.calendar );
986         }
987         else
988         {
989             return false;
990         }
991     }
992 
993 
994     /**
995      * Compares this GeneralizedTime object with the specified GeneralizedTime object.
996      * 
997      * @param other the other GeneralizedTime object
998      * 
999      * @return a negative integer, zero, or a positive integer as this object
1000      *      is less than, equal to, or greater than the specified object.
1001      * 
1002      * @see java.lang.Comparable#compareTo(java.lang.Object)
1003      */
1004     @Override
1005     public int compareTo( GeneralizedTime other )
1006     {
1007         return calendar.compareTo( other.calendar );
1008     }
1009 
1010 
1011     /**
1012      * @return A Date representing the time as milliseconds
1013      */
1014     public long getTime()
1015     {
1016         return calendar.getTimeInMillis();
1017     }
1018 
1019 
1020     /**
1021      * @return A Date representing the time
1022      */
1023     public Date getDate()
1024     {
1025         return calendar.getTime();
1026     }
1027 
1028 
1029     /**
1030      * @return The year part of the date
1031      */
1032     public int getYear()
1033     {
1034         return calendar.get( Calendar.YEAR );
1035     }
1036 
1037 
1038     /**
1039      * @return The month part of the date
1040      */
1041     public int getMonth()
1042     {
1043         return calendar.get( Calendar.MONTH );
1044     }
1045 
1046 
1047     /**
1048      * @return The day part of the date
1049      */
1050     public int getDay()
1051     {
1052         return calendar.get( Calendar.DATE );
1053     }
1054 
1055 
1056     /**
1057      * @return The hours part of the date
1058      */
1059     public int getHour()
1060     {
1061         return calendar.get( Calendar.HOUR_OF_DAY );
1062     }
1063 
1064 
1065     /**
1066      * @return The minutes part of the date
1067      */
1068     public int getMinutes()
1069     {
1070         return calendar.get( Calendar.MINUTE );
1071     }
1072 
1073 
1074     /**
1075      * @return The seconds part of the date
1076      */
1077     public int getSeconds()
1078     {
1079         return calendar.get( Calendar.SECOND );
1080     }
1081 
1082 
1083     /**
1084      * @return The fractional (ie, milliseconds) part of the date
1085      */
1086     public int getFraction()
1087     {
1088         return calendar.get( Calendar.MILLISECOND );
1089     }
1090 
1091 
1092     /**
1093      * Get a Dat einstance from a given String
1094      *
1095      * @param zuluTime The time as a String
1096      * @return A Date instance
1097      * @throws ParseException If the String is not a valid date
1098      */
1099     public static Date getDate( String zuluTime ) throws ParseException
1100     {
1101         try
1102         {
1103             return new GeneralizedTime( zuluTime ).calendar.getTime();
1104         }
1105         catch ( ParseException pe )
1106         {
1107             // Maybe one of the multiple Micro$oft ineptness to cope with Standards ?
1108             if ( "9223372036854775807".equals( zuluTime ) )
1109             {
1110                 // This 0x7FFFFFFFFFFFFFFF, never ending date
1111                 return INFINITE;
1112             }
1113             else
1114             {
1115                 throw pe;
1116             }
1117         }
1118     }
1119 }