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   *    http://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  
23  import java.text.DecimalFormat;
24  import java.text.NumberFormat;
25  import java.text.ParseException;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.TimeZone;
29  
30  import org.apache.directory.api.i18n.I18n;
31  
32  
33  /**
34   * <p>This class represents the generalized time syntax as defined in 
35   * RFC 4517 section 3.3.13.</p>
36   * 
37   * <p>The date, time and time zone information is internally backed
38   * by an {@link java.util.Calendar} object</p>
39   * 
40   * <p>Leap seconds are not supported, as {@link java.util.Calendar}
41   * does not support leap seconds.</p>
42   * 
43   * <pre>
44   * 3.3.13.  Generalized Time
45   *
46   *  A value of the Generalized Time syntax is a character string
47   *  representing a date and time.  The LDAP-specific encoding of a value
48   *  of this syntax is a restriction of the format defined in [ISO8601],
49   *  and is described by the following ABNF:
50   *
51   *     GeneralizedTime = century year month day hour
52   *                          [ minute [ second / leap-second ] ]
53   *                          [ fraction ]
54   *                          g-time-zone
55   *
56   *     century = 2(%x30-39) ; "00" to "99"
57   *     year    = 2(%x30-39) ; "00" to "99"
58   *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
59   *               / ( %x31 %x30-32 ) ; "10" to "12"
60   *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
61   *               / ( %x31-32 %x30-39 ) ; "10" to "29"
62   *               / ( %x33 %x30-31 )    ; "30" to "31"
63   *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
64   *     minute  = %x30-35 %x30-39                        ; "00" to "59"
65   *
66   *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
67   *     leap-second = ( %x36 %x30 )       ; "60"
68   *
69   *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
70   *     g-time-zone     = %x5A  ; "Z"
71   *                       / g-differential
72   *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
73   *     MINUS           = %x2D  ; minus sign ("-")
74   *
75   *  The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
76   *
77   *  The above ABNF allows character strings that do not represent valid
78   *  dates (in the Gregorian calendar) and/or valid times (e.g., February
79   *  31, 1994).  Such character strings SHOULD be considered invalid for
80   *  this syntax.
81   *
82   *  The time value represents coordinated universal time (equivalent to
83   *  Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
84   *  otherwise, the value represents a local time in the time zone
85   *  indicated by <g-differential>.  In the latter case, coordinated
86   *  universal time can be calculated by subtracting the differential from
87   *  the local time.  The "Z" form of <g-time-zone> SHOULD be used in
88   *  preference to <g-differential>.
89   *
90   *  If <minute> is omitted, then <fraction> represents a fraction of an
91   *  hour; otherwise, if <second> and <leap-second> are omitted, then
92   *  <fraction> represents a fraction of a minute; otherwise, <fraction>
93   *  represents a fraction of a second.
94   *
95   *     Examples:
96   *        199412161032Z
97   *        199412160532-0500
98   *
99   *  Both example values represent the same coordinated universal time:
100  *  10:32 AM, December 16, 1994.
101  *
102  *  The LDAP definition for the Generalized Time syntax is:
103  *
104  *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
105  *
106  *  This syntax corresponds to the GeneralizedTime ASN.1 type from
107  *  [ASN.1], with the constraint that local time without a differential
108  *  SHALL NOT be used.
109  *
110  * </pre>
111  */
112 public class GeneralizedTime implements Comparable<GeneralizedTime>
113 {
114 
115     /**
116      * The format of the generalized time.
117      */
118     public enum Format
119     {
120         /** Time format with minutes and seconds, excluding fraction. */
121         YEAR_MONTH_DAY_HOUR_MIN_SEC,
122         /** Time format with minutes and seconds, including fraction. */
123         YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
124 
125         /** Time format with minutes, seconds are omitted, excluding fraction. */
126         YEAR_MONTH_DAY_HOUR_MIN,
127         /** Time format with minutes seconds are omitted, including fraction. */
128         YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
129 
130         /** Time format, minutes and seconds are omitted, excluding fraction. */
131         YEAR_MONTH_DAY_HOUR,
132         /** Time format, minutes and seconds are omitted, including fraction. */
133         YEAR_MONTH_DAY_HOUR_FRACTION
134     }
135 
136     /**
137      * The fraction delimiter of the generalized time.
138      */
139     public enum FractionDelimiter
140     {
141         /** Use a dot as fraction delimiter. */
142         DOT,
143         /** Use a comma as fraction delimiter. */
144         COMMA
145     }
146 
147     /**
148      * The time zone format of the generalized time.
149      */
150     public enum TimeZoneFormat
151     {
152         /** g-time-zone (Zulu) format. */
153         Z,
154         /** g-differential format, using hour only. */
155         DIFF_HOUR,
156         /** g-differential format, using hour and minute. */
157         DIFF_HOUR_MINUTE
158     }
159 
160     private static final TimeZone GMT = TimeZone.getTimeZone( "GMT" );
161 
162     /** The user provided value */
163     private String upGeneralizedTime;
164 
165     /** The user provided format */
166     private Format upFormat;
167 
168     /** The user provided time zone format */
169     private TimeZoneFormat upTimeZoneFormat;
170 
171     /** The user provided fraction delimiter */
172     private FractionDelimiter upFractionDelimiter;
173 
174     /** the user provided fraction length */
175     private int upFractionLength;
176 
177     /** The calendar */
178     private Calendar calendar;
179 
180 
181     /**
182      * 
183      * Creates a new instance of GeneralizedTime by setting the date to an instance of Calendar.
184      * @see #GeneralizedTime(Calendar)
185      * 
186      * @param date the date
187      */
188     public GeneralizedTime( Date date )
189     {
190         calendar = Calendar.getInstance();
191         calendar.setTime( date );
192         setUp( calendar );
193     }
194 
195 
196     /**
197      * Creates a new instance of GeneralizedTime, based on the given Calendar object.
198      * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
199      * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
200      *
201      * @param calendar the calendar containing the date, time and timezone information
202      */
203     public GeneralizedTime( Calendar calendar )
204     {
205         setUp( calendar );
206     }
207 
208 
209     private void setUp( Calendar calendar )
210     {
211         if ( calendar == null )
212         {
213             throw new IllegalArgumentException( I18n.err( I18n.ERR_04358 ) );
214         }
215 
216         this.calendar = calendar;
217         upGeneralizedTime = null;
218         upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
219         upTimeZoneFormat = TimeZoneFormat.Z;
220         upFractionDelimiter = FractionDelimiter.DOT;
221         upFractionLength = 3;
222     }
223 
224 
225     /**
226      * Creates a new instance of GeneralizedTime, based on the
227      * given generalized time string.
228      *
229      * @param generalizedTime the generalized time
230      * 
231      * @throws ParseException if the given generalized time can't be parsed.
232      */
233     public GeneralizedTime( String generalizedTime ) throws ParseException
234     {
235         if ( generalizedTime == null )
236         {
237             throw new ParseException( I18n.err( I18n.ERR_04359 ), 0 );
238         }
239 
240         this.upGeneralizedTime = generalizedTime;
241 
242         calendar = Calendar.getInstance();
243         calendar.setTimeInMillis( 0 );
244         calendar.setLenient( false );
245 
246         parseYear();
247         parseMonth();
248         parseDay();
249         parseHour();
250 
251         if ( upGeneralizedTime.length() < 11 )
252         {
253             throw new ParseException( I18n.err( I18n.ERR_04360 ), 10 );
254         }
255 
256         // pos 10: 
257         // if digit => minute field
258         // if . or , => fraction of hour field
259         // if Z or + or - => timezone field
260         // else error
261         int pos = 10;
262         char c = upGeneralizedTime.charAt( pos );
263         if ( '0' <= c && c <= '9' )
264         {
265             parseMinute();
266 
267             if ( upGeneralizedTime.length() < 13 )
268             {
269                 throw new ParseException( I18n.err( I18n.ERR_04361 ), 12 );
270             }
271 
272             // pos 12: 
273             // if digit => second field
274             // if . or , => fraction of minute field
275             // if Z or + or - => timezone field
276             // else error
277             pos = 12;
278             c = upGeneralizedTime.charAt( pos );
279             if ( '0' <= c && c <= '9' )
280             {
281                 parseSecond();
282 
283                 if ( upGeneralizedTime.length() < 15 )
284                 {
285                     throw new ParseException( I18n.err( I18n.ERR_04362 ), 14 );
286                 }
287 
288                 // pos 14: 
289                 // if . or , => fraction of second field
290                 // if Z or + or - => timezone field
291                 // else error
292                 pos = 14;
293                 c = upGeneralizedTime.charAt( pos );
294                 if ( c == '.' || c == ',' )
295                 {
296                     // read fraction of second
297                     parseFractionOfSecond();
298                     pos += 1 + upFractionLength;
299 
300                     parseTimezone( pos );
301                     upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
302                 }
303                 else if ( c == 'Z' || c == '+' || c == '-' )
304                 {
305                     // read timezone
306                     parseTimezone( pos );
307                     upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
308                 }
309                 else
310                 {
311                     throw new ParseException( I18n.err( I18n.ERR_04363 ), 14 );
312                 }
313             }
314             else if ( c == '.' || c == ',' )
315             {
316                 // read fraction of minute
317                 parseFractionOfMinute();
318                 pos += 1 + upFractionLength;
319 
320                 parseTimezone( pos );
321                 upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
322             }
323             else if ( c == 'Z' || c == '+' || c == '-' )
324             {
325                 // read timezone
326                 parseTimezone( pos );
327                 upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
328             }
329             else
330             {
331                 throw new ParseException( I18n.err( I18n.ERR_04364 ), 12 );
332             }
333         }
334         else if ( c == '.' || c == ',' )
335         {
336             // read fraction of hour
337             parseFractionOfHour();
338             pos += 1 + upFractionLength;
339 
340             parseTimezone( pos );
341             upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
342         }
343         else if ( c == 'Z' || c == '+' || c == '-' )
344         {
345             // read timezone
346             parseTimezone( pos );
347             upFormat = Format.YEAR_MONTH_DAY_HOUR;
348         }
349         else
350         {
351             throw new ParseException( I18n.err( I18n.ERR_04365 ), 10 );
352         }
353 
354         // this calculates and verifies the calendar
355         try
356         {
357             calendar.getTimeInMillis();
358         }
359         catch ( IllegalArgumentException iae )
360         {
361             throw new ParseException( I18n.err( I18n.ERR_04366 ), 0 );
362         }
363 
364         calendar.setLenient( true );
365     }
366 
367 
368     private void parseTimezone( int pos ) throws ParseException
369     {
370         if ( upGeneralizedTime.length() < pos + 1 )
371         {
372             throw new ParseException( I18n.err( I18n.ERR_04367 ), pos );
373         }
374 
375         char c = upGeneralizedTime.charAt( pos );
376         if ( c == 'Z' )
377         {
378             calendar.setTimeZone( GMT );
379             upTimeZoneFormat = TimeZoneFormat.Z;
380 
381             if ( upGeneralizedTime.length() > pos + 1 )
382             {
383                 throw new ParseException( I18n.err( I18n.ERR_04368 ), pos + 1 );
384             }
385         }
386         else if ( c == '+' || c == '-' )
387         {
388             StringBuilder sb = new StringBuilder( "GMT" );
389             sb.append( c );
390 
391             String digits = getAllDigits( pos + 1 );
392             sb.append( digits );
393 
394             if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
395             {
396                 TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
397                 calendar.setTimeZone( timeZone );
398                 upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
399             }
400             else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
401             {
402                 TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
403                 calendar.setTimeZone( timeZone );
404                 upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
405             }
406             else
407             {
408                 throw new ParseException( I18n.err( I18n.ERR_04369 ), pos );
409             }
410 
411             if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
412             {
413                 throw new ParseException( I18n.err( I18n.ERR_04370 ), pos + 1 + digits.length() );
414             }
415         }
416     }
417 
418 
419     private void parseFractionOfSecond() throws ParseException
420     {
421         parseFractionDelmiter( 14 );
422         String fraction = getFraction( 14 + 1 );
423         upFractionLength = fraction.length();
424 
425         double fract = Double.parseDouble( "0." + fraction );
426         int millisecond = ( int ) Math.round( fract * 1000 );
427 
428         calendar.set( Calendar.MILLISECOND, millisecond );
429     }
430 
431 
432     private void parseFractionOfMinute() throws ParseException
433     {
434         parseFractionDelmiter( 12 );
435         String fraction = getFraction( 12 + 1 );
436         upFractionLength = fraction.length();
437 
438         double fract = Double.parseDouble( "0." + fraction );
439         int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
440         int second = milliseconds / 1000;
441         int millisecond = milliseconds - ( second * 1000 );
442 
443         calendar.set( Calendar.SECOND, second );
444         calendar.set( Calendar.MILLISECOND, millisecond );
445     }
446 
447 
448     private void parseFractionOfHour() throws ParseException
449     {
450         parseFractionDelmiter( 10 );
451         String fraction = getFraction( 10 + 1 );
452         upFractionLength = fraction.length();
453 
454         double fract = Double.parseDouble( "0." + fraction );
455         int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
456         int minute = milliseconds / ( 1000 * 60 );
457         int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
458         int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
459 
460         calendar.set( Calendar.MINUTE, minute );
461         calendar.set( Calendar.SECOND, second );
462         calendar.set( Calendar.MILLISECOND, millisecond );
463     }
464 
465 
466     private void parseFractionDelmiter( int fractionDelimiterPos )
467     {
468         char c = upGeneralizedTime.charAt( fractionDelimiterPos );
469         upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
470     }
471 
472 
473     private String getFraction( int startIndex ) throws ParseException
474     {
475         String fraction = getAllDigits( startIndex );
476 
477         // minimum one digit
478         if ( fraction.length() == 0 )
479         {
480             throw new ParseException( I18n.err( I18n.ERR_04371 ), startIndex );
481         }
482 
483         return fraction;
484     }
485 
486 
487     private String getAllDigits( int startIndex )
488     {
489         StringBuilder sb = new StringBuilder();
490         while ( upGeneralizedTime.length() > startIndex )
491         {
492             char c = upGeneralizedTime.charAt( startIndex );
493             if ( '0' <= c && c <= '9' )
494             {
495                 sb.append( c );
496                 startIndex++;
497             }
498             else
499             {
500                 break;
501             }
502         }
503         return sb.toString();
504     }
505 
506 
507     private void parseSecond() throws ParseException
508     {
509         // read minute
510         if ( upGeneralizedTime.length() < 14 )
511         {
512             throw new ParseException( I18n.err( I18n.ERR_04372 ), 12 );
513         }
514         try
515         {
516             int second = Integer.parseInt( upGeneralizedTime.substring( 12, 14 ) );
517             calendar.set( Calendar.SECOND, second );
518         }
519         catch ( NumberFormatException e )
520         {
521             throw new ParseException( I18n.err( I18n.ERR_04373 ), 12 );
522         }
523     }
524 
525 
526     private void parseMinute() throws ParseException
527     {
528         // read minute
529         if ( upGeneralizedTime.length() < 12 )
530         {
531             throw new ParseException( I18n.err( I18n.ERR_04374 ), 10 );
532         }
533         try
534         {
535             int minute = Integer.parseInt( upGeneralizedTime.substring( 10, 12 ) );
536             calendar.set( Calendar.MINUTE, minute );
537         }
538         catch ( NumberFormatException e )
539         {
540             throw new ParseException( I18n.err( I18n.ERR_04375 ), 10 );
541         }
542     }
543 
544 
545     private void parseHour() throws ParseException
546     {
547         if ( upGeneralizedTime.length() < 10 )
548         {
549             throw new ParseException( I18n.err( I18n.ERR_04376 ), 8 );
550         }
551         try
552         {
553             int hour = Integer.parseInt( upGeneralizedTime.substring( 8, 10 ) );
554             calendar.set( Calendar.HOUR_OF_DAY, hour );
555         }
556         catch ( NumberFormatException e )
557         {
558             throw new ParseException( I18n.err( I18n.ERR_04377 ), 8 );
559         }
560     }
561 
562 
563     private void parseDay() throws ParseException
564     {
565         if ( upGeneralizedTime.length() < 8 )
566         {
567             throw new ParseException( I18n.err( I18n.ERR_04378 ), 6 );
568         }
569         try
570         {
571             int day = Integer.parseInt( upGeneralizedTime.substring( 6, 8 ) );
572             calendar.set( Calendar.DAY_OF_MONTH, day );
573         }
574         catch ( NumberFormatException e )
575         {
576             throw new ParseException( I18n.err( I18n.ERR_04379 ), 6 );
577         }
578     }
579 
580 
581     private void parseMonth() throws ParseException
582     {
583         if ( upGeneralizedTime.length() < 6 )
584         {
585             throw new ParseException( I18n.err( I18n.ERR_04380 ), 4 );
586         }
587         try
588         {
589             int month = Integer.parseInt( upGeneralizedTime.substring( 4, 6 ) );
590             calendar.set( Calendar.MONTH, month - 1 );
591         }
592         catch ( NumberFormatException e )
593         {
594             throw new ParseException( I18n.err( I18n.ERR_04381 ), 4 );
595         }
596     }
597 
598 
599     private void parseYear() throws ParseException
600     {
601         if ( upGeneralizedTime.length() < 4 )
602         {
603             throw new ParseException( I18n.err( I18n.ERR_04382 ), 0 );
604         }
605         try
606         {
607             int year = Integer.parseInt( upGeneralizedTime.substring( 0, 4 ) );
608             calendar.set( Calendar.YEAR, year );
609         }
610         catch ( NumberFormatException e )
611         {
612             throw new ParseException( I18n.err( I18n.ERR_04383 ), 0 );
613         }
614     }
615 
616 
617     /**
618      * Returns the string representation of this generalized time. 
619      * This method uses the same format as the user provided format.
620      *
621      * @return the string representation of this generalized time
622      */
623     public String toGeneralizedTime()
624     {
625         return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
626     }
627 
628 
629     /**
630      * Returns the string representation of this generalized time. 
631      * This method uses the same format as the user provided format.
632      *
633      * @return the string representation of this generalized time
634      */
635     public String toGeneralizedTimeWithoutFraction()
636     {
637         return toGeneralizedTime( getFormatWithoutFraction( upFormat ), upFractionDelimiter, upFractionLength,
638             upTimeZoneFormat );
639     }
640 
641 
642     /**
643      * Gets the corresponding format with fraction.
644      *
645      * @param f the format
646      * @return the corresponding format without fraction
647      */
648     private Format getFormatWithoutFraction( Format f )
649     {
650         switch ( f )
651         {
652             case YEAR_MONTH_DAY_HOUR_FRACTION:
653                 return Format.YEAR_MONTH_DAY_HOUR;
654             case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
655                 return Format.YEAR_MONTH_DAY_HOUR_MIN;
656             case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
657                 return Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
658             default:
659                 break;
660         }
661 
662         return f;
663     }
664 
665 
666     /**
667      * Returns the string representation of this generalized time.
668      * 
669      * @param format the target format
670      * @param fractionDelimiter the target fraction delimiter, may be null
671      * @param fractionLength the fraction length
672      * @param timeZoneFormat the target time zone format
673      * 
674      * @return the string
675      */
676     public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
677         TimeZoneFormat timeZoneFormat )
678     {
679         Calendar clonedCalendar = ( Calendar ) this.calendar.clone();
680         if ( timeZoneFormat == TimeZoneFormat.Z )
681         {
682             clonedCalendar.setTimeZone( GMT );
683         }
684 
685         NumberFormat twoDigits = new DecimalFormat( "00" );
686         NumberFormat fourDigits = new DecimalFormat( "00" );
687         StringBuffer fractionFormat = new StringBuffer( "" );
688         for ( int i = 0; i < fractionLength && i < 3; i++ )
689         {
690             fractionFormat.append( "0" );
691         }
692 
693         StringBuilder sb = new StringBuilder();
694         sb.append( fourDigits.format( clonedCalendar.get( Calendar.YEAR ) ) );
695         sb.append( twoDigits.format( clonedCalendar.get( Calendar.MONTH ) + 1 ) );
696         sb.append( twoDigits.format( clonedCalendar.get( Calendar.DAY_OF_MONTH ) ) );
697         sb.append( twoDigits.format( clonedCalendar.get( Calendar.HOUR_OF_DAY ) ) );
698 
699         switch ( format )
700         {
701             case YEAR_MONTH_DAY_HOUR_MIN_SEC:
702                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
703                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.SECOND ) ) );
704                 break;
705 
706             case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
707                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
708                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.SECOND ) ) );
709 
710                 NumberFormat fractionDigits = new DecimalFormat( fractionFormat.toString() );
711                 sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
712                 sb.append( fractionDigits.format( clonedCalendar.get( Calendar.MILLISECOND ) ) );
713                 break;
714 
715             case YEAR_MONTH_DAY_HOUR_MIN:
716                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
717                 break;
718 
719             case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
720                 sb.append( twoDigits.format( clonedCalendar.get( Calendar.MINUTE ) ) );
721 
722                 // sec + millis => fraction of minute
723                 double millisec = 1000 * clonedCalendar.get( Calendar.SECOND )
724                     + clonedCalendar.get( Calendar.MILLISECOND );
725                 double fraction = millisec / ( 1000 * 60 );
726                 fractionDigits = new DecimalFormat( "0." + fractionFormat );
727                 sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
728                 sb.append( fractionDigits.format( fraction ).substring( 2 ) );
729                 break;
730 
731             case YEAR_MONTH_DAY_HOUR_FRACTION:
732                 // min + sec + millis => fraction of minute
733                 millisec = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000
734                     * clonedCalendar.get( Calendar.SECOND )
735                     + clonedCalendar.get( Calendar.MILLISECOND );
736                 fraction = millisec / ( 1000 * 60 * 60 );
737                 fractionDigits = new DecimalFormat( "0." + fractionFormat );
738                 sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
739                 sb.append( fractionDigits.format( fraction ).substring( 2 ) );
740 
741                 break;
742         }
743 
744         if ( timeZoneFormat == TimeZoneFormat.Z && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
745         {
746             sb.append( 'Z' );
747         }
748         else
749         {
750             TimeZone timeZone = clonedCalendar.getTimeZone();
751             int rawOffset = timeZone.getRawOffset();
752             sb.append( rawOffset < 0 ? '-' : '+' );
753 
754             rawOffset = Math.abs( rawOffset );
755             int hour = rawOffset / ( 60 * 60 * 1000 );
756             int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
757 
758             if ( hour < 10 )
759             {
760                 sb.append( '0' );
761             }
762             sb.append( hour );
763 
764             if ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE || timeZoneFormat == TimeZoneFormat.Z )
765             {
766                 if ( minute < 10 )
767                 {
768                     sb.append( '0' );
769                 }
770                 sb.append( minute );
771             }
772         }
773 
774         return sb.toString();
775     }
776 
777 
778     /**
779      * Gets the calendar. It could be used to manipulate this 
780      * {@link GeneralizedTime} settings.
781      * 
782      * @return the calendar
783      */
784     public Calendar getCalendar()
785     {
786         return calendar;
787     }
788 
789 
790     @Override
791     public String toString()
792     {
793         return toGeneralizedTime();
794     }
795 
796 
797     @Override
798     public int hashCode()
799     {
800         final int prime = 31;
801         int result = 1;
802         result = prime * result + calendar.hashCode();
803         return result;
804     }
805 
806 
807     @Override
808     public boolean equals( Object obj )
809     {
810         if ( obj instanceof GeneralizedTime )
811         {
812             GeneralizedTime other = ( GeneralizedTime ) obj;
813             return calendar.equals( other.calendar );
814         }
815         else
816         {
817             return false;
818         }
819     }
820 
821 
822     /**
823      * Compares this GeneralizedTime object with the specified GeneralizedTime object.
824      * 
825      * @param other the other GeneralizedTime object
826      * 
827      * @return a negative integer, zero, or a positive integer as this object
828      *      is less than, equal to, or greater than the specified object.
829      * 
830      * @see java.lang.Comparable#compareTo(java.lang.Object)
831      */
832     public int compareTo( GeneralizedTime other )
833     {
834         return calendar.compareTo( other.calendar );
835     }
836 
837 
838     public long getTime()
839     {
840         return calendar.getTimeInMillis();
841     }
842 
843 
844     public Date getDate()
845     {
846         return calendar.getTime();
847     }
848 
849 
850     public int getYear()
851     {
852         return calendar.get( Calendar.YEAR );
853     }
854 
855 
856     public int getMonth()
857     {
858         return calendar.get( Calendar.MONTH );
859     }
860 
861 
862     public int getDay()
863     {
864         return calendar.get( Calendar.DATE );
865     }
866 
867 
868     public int getHour()
869     {
870         return calendar.get( Calendar.HOUR_OF_DAY );
871     }
872 
873 
874     public int getMinutes()
875     {
876         return calendar.get( Calendar.MINUTE );
877     }
878 
879 
880     public int getSeconds()
881     {
882         return calendar.get( Calendar.SECOND );
883     }
884 
885 
886     public int getFraction()
887     {
888         return calendar.get( Calendar.MILLISECOND );
889     }
890 
891 
892     /**
893      * 
894      *
895      * @param zuluTime
896      * @return
897      */
898     public static Date getDate( String zuluTime ) throws ParseException
899     {
900         return new GeneralizedTime( zuluTime ).calendar.getTime();
901     }
902 }