001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *  
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *  
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License. 
018 *  
019 */
020package org.apache.directory.api.util;
021
022
023import java.text.DecimalFormat;
024import java.text.NumberFormat;
025import java.text.ParseException;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.TimeZone;
029
030import org.apache.directory.api.i18n.I18n;
031
032
033/**
034 * <p>This class represents the generalized time syntax as defined in 
035 * RFC 4517 section 3.3.13.</p>
036 * 
037 * <p>The date, time and time zone information is internally backed
038 * by an {@link java.util.Calendar} object</p>
039 * 
040 * <p>Leap seconds are not supported, as {@link java.util.Calendar}
041 * does not support leap seconds.</p>
042 * 
043 * <pre>
044 * 3.3.13.  Generalized Time
045 *
046 *  A value of the Generalized Time syntax is a character string
047 *  representing a date and time.  The LDAP-specific encoding of a value
048 *  of this syntax is a restriction of the format defined in [ISO8601],
049 *  and is described by the following ABNF:
050 *
051 *     GeneralizedTime = century year month day hour
052 *                          [ minute [ second / leap-second ] ]
053 *                          [ fraction ]
054 *                          g-time-zone
055 *
056 *     century = 2(%x30-39) ; "00" to "99"
057 *     year    = 2(%x30-39) ; "00" to "99"
058 *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
059 *               / ( %x31 %x30-32 ) ; "10" to "12"
060 *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
061 *               / ( %x31-32 %x30-39 ) ; "10" to "29"
062 *               / ( %x33 %x30-31 )    ; "30" to "31"
063 *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
064 *     minute  = %x30-35 %x30-39                        ; "00" to "59"
065 *
066 *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
067 *     leap-second = ( %x36 %x30 )       ; "60"
068 *
069 *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
070 *     g-time-zone     = %x5A  ; "Z"
071 *                       / g-differential
072 *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
073 *     MINUS           = %x2D  ; minus sign ("-")
074 *
075 *  The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
076 *
077 *  The above ABNF allows character strings that do not represent valid
078 *  dates (in the Gregorian calendar) and/or valid times (e.g., February
079 *  31, 1994).  Such character strings SHOULD be considered invalid for
080 *  this syntax.
081 *
082 *  The time value represents coordinated universal time (equivalent to
083 *  Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
084 *  otherwise, the value represents a local time in the time zone
085 *  indicated by <g-differential>.  In the latter case, coordinated
086 *  universal time can be calculated by subtracting the differential from
087 *  the local time.  The "Z" form of <g-time-zone> SHOULD be used in
088 *  preference to <g-differential>.
089 *
090 *  If <minute> is omitted, then <fraction> represents a fraction of an
091 *  hour; otherwise, if <second> and <leap-second> are omitted, then
092 *  <fraction> represents a fraction of a minute; otherwise, <fraction>
093 *  represents a fraction of a second.
094 *
095 *     Examples:
096 *        199412161032Z
097 *        199412160532-0500
098 *
099 *  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 */
112public 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}