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.ParseException;
024import java.util.Calendar;
025import java.util.Date;
026import java.util.GregorianCalendar;
027import java.util.Locale;
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     * The format of the generalized time.
116     */
117    public enum Format
118    {
119        /** Time format with minutes and seconds, excluding fraction. */
120        YEAR_MONTH_DAY_HOUR_MIN_SEC,
121        /** Time format with minutes and seconds, including fraction. */
122        YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
123
124        /** Time format with minutes, seconds are omitted, excluding fraction. */
125        YEAR_MONTH_DAY_HOUR_MIN,
126        /** Time format with minutes seconds are omitted, including fraction. */
127        YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
128
129        /** Time format, minutes and seconds are omitted, excluding fraction. */
130        YEAR_MONTH_DAY_HOUR,
131        /** Time format, minutes and seconds are omitted, including fraction. */
132        YEAR_MONTH_DAY_HOUR_FRACTION
133    }
134
135    /**
136     * The fraction delimiter of the generalized time.
137     */
138    public enum FractionDelimiter
139    {
140        /** Use a dot as fraction delimiter. */
141        DOT,
142        /** Use a comma as fraction delimiter. */
143        COMMA
144    }
145
146    /**
147     * The time zone format of the generalized time.
148     */
149    public enum TimeZoneFormat
150    {
151        /** g-time-zone (Zulu) format. */
152        Z,
153        /** g-differential format, using hour only. */
154        DIFF_HOUR,
155        /** g-differential format, using hour and minute. */
156        DIFF_HOUR_MINUTE
157    }
158
159    /** The GMT TimeZone */
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 = new GregorianCalendar( GMT, Locale.ROOT );
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 newCalendar )
210    {
211        if ( newCalendar == null )
212        {
213            throw new IllegalArgumentException( I18n.err( I18n.ERR_04358 ) );
214        }
215
216        this.calendar = newCalendar;
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 = new GregorianCalendar( GMT, Locale.ROOT );
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        
264        if ( ( '0' <= c ) && ( c <= '9' ) )
265        {
266            parseMinute();
267
268            if ( upGeneralizedTime.length() < 13 )
269            {
270                throw new ParseException( I18n.err( I18n.ERR_04361 ), 12 );
271            }
272
273            // pos 12: 
274            // if digit => second field
275            // if . or , => fraction of minute field
276            // if Z or + or - => timezone field
277            // else error
278            pos = 12;
279            c = upGeneralizedTime.charAt( pos );
280            
281            if ( ( '0' <= c ) && ( c <= '9' ) )
282            {
283                parseSecond();
284
285                if ( upGeneralizedTime.length() < 15 )
286                {
287                    throw new ParseException( I18n.err( I18n.ERR_04362 ), 14 );
288                }
289
290                // pos 14: 
291                // if . or , => fraction of second field
292                // if Z or + or - => timezone field
293                // else error
294                pos = 14;
295                c = upGeneralizedTime.charAt( pos );
296                
297                if ( ( c == '.' ) || ( c == ',' ) )
298                {
299                    // read fraction of second
300                    parseFractionOfSecond();
301                    pos += 1 + upFractionLength;
302
303                    parseTimezone( pos );
304                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
305                }
306                else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
307                {
308                    // read timezone
309                    parseTimezone( pos );
310                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
311                }
312                else
313                {
314                    throw new ParseException( I18n.err( I18n.ERR_04363 ), 14 );
315                }
316            }
317            else if ( ( c == '.' ) || ( c == ',' ) )
318            {
319                // read fraction of minute
320                parseFractionOfMinute();
321                pos += 1 + upFractionLength;
322
323                parseTimezone( pos );
324                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
325            }
326            else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
327            {
328                // read timezone
329                parseTimezone( pos );
330                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
331            }
332            else
333            {
334                throw new ParseException( I18n.err( I18n.ERR_04364 ), 12 );
335            }
336        }
337        else if ( ( c == '.' ) || ( c == ',' ) )
338        {
339            // read fraction of hour
340            parseFractionOfHour();
341            pos += 1 + upFractionLength;
342
343            parseTimezone( pos );
344            upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
345        }
346        else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
347        {
348            // read timezone
349            parseTimezone( pos );
350            upFormat = Format.YEAR_MONTH_DAY_HOUR;
351        }
352        else
353        {
354            throw new ParseException( I18n.err( I18n.ERR_04365 ), 10 );
355        }
356
357        // this calculates and verifies the calendar
358        /* Not sure we should do that... */
359        try
360        {
361            calendar.getTimeInMillis();
362        }
363        catch ( IllegalArgumentException iae )
364        {
365            throw new ParseException( I18n.err( I18n.ERR_04366 ), 0 );
366        }
367
368        calendar.setLenient( true );
369    }
370
371
372    private void parseTimezone( int pos ) throws ParseException
373    {
374        if ( upGeneralizedTime.length() < pos + 1 )
375        {
376            throw new ParseException( I18n.err( I18n.ERR_04367 ), pos );
377        }
378
379        char c = upGeneralizedTime.charAt( pos );
380        
381        if ( c == 'Z' )
382        {
383            calendar.setTimeZone( GMT );
384            upTimeZoneFormat = TimeZoneFormat.Z;
385
386            if ( upGeneralizedTime.length() > pos + 1 )
387            {
388                throw new ParseException( I18n.err( I18n.ERR_04368 ), pos + 1 );
389            }
390        }
391        else if ( ( c == '+' ) || ( c == '-' ) )
392        {
393            StringBuilder sb = new StringBuilder( "GMT" );
394            sb.append( c );
395
396            String digits = getAllDigits( pos + 1 );
397            sb.append( digits );
398
399            if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
400            {
401                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
402                calendar.setTimeZone( timeZone );
403                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
404            }
405            else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
406            {
407                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
408                calendar.setTimeZone( timeZone );
409                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
410            }
411            else
412            {
413                throw new ParseException( I18n.err( I18n.ERR_04369 ), pos );
414            }
415
416            if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
417            {
418                throw new ParseException( I18n.err( I18n.ERR_04370 ), pos + 1 + digits.length() );
419            }
420        }
421    }
422
423
424    private void parseFractionOfSecond() throws ParseException
425    {
426        parseFractionDelmiter( 14 );
427        String fraction = getFraction( 14 + 1 );
428        upFractionLength = fraction.length();
429
430        double fract = Double.parseDouble( "0." + fraction );
431        int millisecond = ( int ) Math.floor( fract * 1000 );
432
433        calendar.set( GregorianCalendar.MILLISECOND, millisecond );
434    }
435
436
437    private void parseFractionOfMinute() throws ParseException
438    {
439        parseFractionDelmiter( 12 );
440        String fraction = getFraction( 12 + 1 );
441        upFractionLength = fraction.length();
442
443        double fract = Double.parseDouble( "0." + fraction );
444        int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
445        int second = milliseconds / 1000;
446        int millisecond = milliseconds - ( second * 1000 );
447
448        calendar.set( Calendar.SECOND, second );
449        calendar.set( Calendar.MILLISECOND, millisecond );
450    }
451
452
453    private void parseFractionOfHour() throws ParseException
454    {
455        parseFractionDelmiter( 10 );
456        String fraction = getFraction( 10 + 1 );
457        upFractionLength = fraction.length();
458
459        double fract = Double.parseDouble( "0." + fraction );
460        int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
461        int minute = milliseconds / ( 1000 * 60 );
462        int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
463        int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
464
465        calendar.set( Calendar.MINUTE, minute );
466        calendar.set( Calendar.SECOND, second );
467        calendar.set( Calendar.MILLISECOND, millisecond );
468    }
469
470
471    private void parseFractionDelmiter( int fractionDelimiterPos )
472    {
473        char c = upGeneralizedTime.charAt( fractionDelimiterPos );
474        upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
475    }
476
477
478    private String getFraction( int startIndex ) throws ParseException
479    {
480        String fraction = getAllDigits( startIndex );
481
482        // minimum one digit
483        if ( fraction.length() == 0 )
484        {
485            throw new ParseException( I18n.err( I18n.ERR_04371 ), startIndex );
486        }
487
488        return fraction;
489    }
490
491
492    private String getAllDigits( int startIndex )
493    {
494        StringBuilder sb = new StringBuilder();
495        while ( upGeneralizedTime.length() > startIndex )
496        {
497            char c = upGeneralizedTime.charAt( startIndex );
498            if ( '0' <= c && c <= '9' )
499            {
500                sb.append( c );
501                startIndex++;
502            }
503            else
504            {
505                break;
506            }
507        }
508        return sb.toString();
509    }
510
511
512    private void parseSecond() throws ParseException
513    {
514        // read minute
515        if ( upGeneralizedTime.length() < 14 )
516        {
517            throw new ParseException( I18n.err( I18n.ERR_04372 ), 12 );
518        }
519        try
520        {
521            int second = Strings.parseInt( upGeneralizedTime.substring( 12, 14 ) );
522            calendar.set( Calendar.SECOND, second );
523        }
524        catch ( NumberFormatException e )
525        {
526            throw new ParseException( I18n.err( I18n.ERR_04373 ), 12 );
527        }
528    }
529
530
531    private void parseMinute() throws ParseException
532    {
533        // read minute
534        if ( upGeneralizedTime.length() < 12 )
535        {
536            throw new ParseException( I18n.err( I18n.ERR_04374 ), 10 );
537        }
538        try
539        {
540            int minute = Strings.parseInt( upGeneralizedTime.substring( 10, 12 ) );
541            calendar.set( Calendar.MINUTE, minute );
542        }
543        catch ( NumberFormatException e )
544        {
545            throw new ParseException( I18n.err( I18n.ERR_04375 ), 10 );
546        }
547    }
548
549
550    private void parseHour() throws ParseException
551    {
552        if ( upGeneralizedTime.length() < 10 )
553        {
554            throw new ParseException( I18n.err( I18n.ERR_04376 ), 8 );
555        }
556        try
557        {
558            int hour = Strings.parseInt( upGeneralizedTime.substring( 8, 10 ) );
559            calendar.set( Calendar.HOUR_OF_DAY, hour );
560        }
561        catch ( NumberFormatException e )
562        {
563            throw new ParseException( I18n.err( I18n.ERR_04377 ), 8 );
564        }
565    }
566
567
568    private void parseDay() throws ParseException
569    {
570        if ( upGeneralizedTime.length() < 8 )
571        {
572            throw new ParseException( I18n.err( I18n.ERR_04378 ), 6 );
573        }
574        try
575        {
576            int day = Strings.parseInt( upGeneralizedTime.substring( 6, 8 ) );
577            calendar.set( Calendar.DAY_OF_MONTH, day );
578        }
579        catch ( NumberFormatException e )
580        {
581            throw new ParseException( I18n.err( I18n.ERR_04379 ), 6 );
582        }
583    }
584
585
586    private void parseMonth() throws ParseException
587    {
588        if ( upGeneralizedTime.length() < 6 )
589        {
590            throw new ParseException( I18n.err( I18n.ERR_04380 ), 4 );
591        }
592        try
593        {
594            int month = Strings.parseInt( upGeneralizedTime.substring( 4, 6 ) );
595            calendar.set( Calendar.MONTH, month - 1 );
596        }
597        catch ( NumberFormatException e )
598        {
599            throw new ParseException( I18n.err( I18n.ERR_04381 ), 4 );
600        }
601    }
602
603
604    private void parseYear() throws ParseException
605    {
606        if ( upGeneralizedTime.length() < 4 )
607        {
608            throw new ParseException( I18n.err( I18n.ERR_04382 ), 0 );
609        }
610        try
611        {
612            int year = Strings.parseInt( upGeneralizedTime.substring( 0, 4 ) );
613            calendar.set( Calendar.YEAR, year );
614        }
615        catch ( NumberFormatException e )
616        {
617            throw new ParseException( I18n.err( I18n.ERR_04383 ), 0 );
618        }
619    }
620
621
622    /**
623     * Returns the string representation of this generalized time. 
624     * This method uses the same format as the user provided format.
625     *
626     * @return the string representation of this generalized time
627     */
628    public String toGeneralizedTime()
629    {
630        return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
631    }
632
633
634    /**
635     * Returns the string representation of this generalized time. 
636     * This method uses the same format as the user provided format.
637     *
638     * @return the string representation of this generalized time
639     */
640    public String toGeneralizedTimeWithoutFraction()
641    {
642        return toGeneralizedTime( getFormatWithoutFraction( upFormat ), upFractionDelimiter, upFractionLength,
643            upTimeZoneFormat );
644    }
645
646
647    /**
648     * Gets the corresponding format with fraction.
649     *
650     * @param f the format
651     * @return the corresponding format without fraction
652     */
653    private Format getFormatWithoutFraction( Format f )
654    {
655        switch ( f )
656        {
657            case YEAR_MONTH_DAY_HOUR_FRACTION:
658                return Format.YEAR_MONTH_DAY_HOUR;
659            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
660                return Format.YEAR_MONTH_DAY_HOUR_MIN;
661            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
662                return Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
663            default:
664                break;
665        }
666
667        return f;
668    }
669
670
671    /**
672     * Returns the string representation of this generalized time.
673     * 
674     * @param format the target format
675     * @param fractionDelimiter the target fraction delimiter, may be null
676     * @param fractionLength the fraction length
677     * @param timeZoneFormat the target time zone format
678     * 
679     * @return the string
680     */
681    public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
682        TimeZoneFormat timeZoneFormat )
683    {
684        Calendar clonedCalendar = ( Calendar ) calendar.clone();
685
686        if ( timeZoneFormat == TimeZoneFormat.Z )
687        {
688            clonedCalendar.setTimeZone( GMT );
689        }
690
691        // Create the result. It can contain a maximum of 23 chars
692        byte[] result = new byte[23];
693
694        // The starting point
695        int pos = 0;
696
697        // Inject the year
698        int year = clonedCalendar.get( Calendar.YEAR );
699
700        result[pos++] = ( byte ) ( ( year / 1000 ) + '0' );
701        year %= 1000;
702
703        result[pos++] = ( byte ) ( ( year / 100 ) + '0' );
704        year %= 100;
705
706        result[pos++] = ( byte ) ( ( year / 10 ) + '0' );
707
708        result[pos++] = ( byte ) ( ( year % 10 ) + '0' );
709
710        // Inject the month
711        int month = clonedCalendar.get( Calendar.MONTH ) + 1;
712
713        result[pos++] = ( byte ) ( ( month / 10 ) + '0' );
714
715        result[pos++] = ( byte ) ( ( month % 10 ) + '0' );
716
717        // Inject the day
718        int day = clonedCalendar.get( Calendar.DAY_OF_MONTH );
719
720        result[pos++] = ( byte ) ( ( day / 10 ) + '0' );
721
722        result[pos++] = ( byte ) ( ( day % 10 ) + '0' );
723
724        // Inject the hour
725        int hour = clonedCalendar.get( Calendar.HOUR_OF_DAY );
726
727        result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
728
729        result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
730
731        switch ( format )
732        {
733            case YEAR_MONTH_DAY_HOUR_MIN_SEC:
734                // Inject the minutes
735                int minute = clonedCalendar.get( Calendar.MINUTE );
736
737                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
738
739                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
740
741                // Inject the seconds
742                int second = clonedCalendar.get( Calendar.SECOND );
743
744                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
745
746                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
747
748                break;
749
750            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
751                // Inject the minutes
752                minute = clonedCalendar.get( Calendar.MINUTE );
753
754                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
755
756                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
757
758                // Inject the seconds
759                second = clonedCalendar.get( Calendar.SECOND );
760
761                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
762
763                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
764
765                // Inject the fraction
766                if ( fractionDelimiter == FractionDelimiter.COMMA )
767                {
768                    result[pos++] = ',';
769                }
770                else
771                {
772                    result[pos++] = '.';
773                }
774
775                // Inject the fraction
776                int millisecond = clonedCalendar.get( Calendar.MILLISECOND );
777
778                result[pos++] = ( byte ) ( ( millisecond / 100 ) + '0' );
779                millisecond %= 100;
780
781                result[pos++] = ( byte ) ( ( millisecond / 10 ) + '0' );
782
783                //if ( millisecond > 0 )
784                result[pos++] = ( byte ) ( ( millisecond % 10 ) + '0' );
785
786                break;
787
788            case YEAR_MONTH_DAY_HOUR_MIN:
789                // Inject the minutes
790                minute = clonedCalendar.get( Calendar.MINUTE );
791
792                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
793
794                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
795                break;
796
797            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
798                // Inject the minutes
799                minute = clonedCalendar.get( Calendar.MINUTE );
800
801                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
802
803                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
804
805                // sec + millis => fraction of a minute
806                int fraction = 1000 * clonedCalendar.get( Calendar.SECOND )
807                    + clonedCalendar.get( Calendar.MILLISECOND );
808                fraction /= 60;
809
810                if ( fraction > 0 )
811                {
812                    if ( fractionDelimiter == FractionDelimiter.COMMA )
813                    {
814                        result[pos++] = ',';
815                    }
816                    else
817                    {
818                        result[pos++] = '.';
819                    }
820
821                    // At this point, the fraction should be in [999, 1]
822                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
823                    fraction %= 100;
824
825                    if ( fraction > 0 )
826                    {
827                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
828
829                        if ( fraction > 0 )
830                        {
831                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
832                        }
833                    }
834                }
835
836                break;
837
838            case YEAR_MONTH_DAY_HOUR:
839                // nothing to add
840                break;
841
842            case YEAR_MONTH_DAY_HOUR_FRACTION:
843                // min + sec + millis => fraction of an hour
844                fraction = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000
845                    * clonedCalendar.get( Calendar.SECOND )
846                    + clonedCalendar.get( Calendar.MILLISECOND );
847                fraction /= 60 * 60;
848
849                // At this point, the fraction should be in [999, 1]
850                if ( fraction > 0 )
851                {
852                    if ( fractionDelimiter == FractionDelimiter.COMMA )
853                    {
854                        result[pos++] = ',';
855                    }
856                    else
857                    {
858                        result[pos++] = '.';
859                    }
860
861                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
862                    fraction %= 100;
863
864                    if ( fraction > 0 )
865                    {
866                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
867
868                        if ( fraction > 0 )
869                        {
870                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
871                        }
872                    }
873                }
874
875                break;
876
877            default:
878                throw new IllegalArgumentException( "Unexpected format " + format );
879        }
880
881        if ( ( timeZoneFormat == TimeZoneFormat.Z ) && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
882        {
883            result[pos++] = 'Z';
884        }
885        else
886        {
887            // g-differential
888            TimeZone timeZone = clonedCalendar.getTimeZone();
889            int rawOffset = timeZone.getRawOffset();
890
891            if ( rawOffset < 0 )
892            {
893                result[pos++] = '-';
894            }
895            else
896            {
897                result[pos++] = '+';
898            }
899
900            rawOffset = Math.abs( rawOffset );
901            hour = rawOffset / ( 60 * 60 * 1000 );
902            int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
903
904            // The offset hour
905            result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
906
907            result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
908
909            if ( ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE ) || ( timeZoneFormat == TimeZoneFormat.Z ) )
910            {
911                // The offset minute
912                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
913
914                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
915            }
916        }
917
918        return Strings.utf8ToString( result, 0, pos );
919    }
920
921
922    /**
923     * Gets the calendar. It could be used to manipulate this 
924     * {@link GeneralizedTime} settings.
925     * 
926     * @return the calendar
927     */
928    public Calendar getCalendar()
929    {
930        return calendar;
931    }
932
933
934    @Override
935    public String toString()
936    {
937        return toGeneralizedTime();
938    }
939
940
941    @Override
942    public int hashCode()
943    {
944        final int prime = 31;
945        int result = 1;
946        result = prime * result + calendar.hashCode();
947        return result;
948    }
949
950
951    @Override
952    public boolean equals( Object obj )
953    {
954        if ( obj instanceof GeneralizedTime )
955        {
956            GeneralizedTime other = ( GeneralizedTime ) obj;
957            return calendar.equals( other.calendar );
958        }
959        else
960        {
961            return false;
962        }
963    }
964
965
966    /**
967     * Compares this GeneralizedTime object with the specified GeneralizedTime object.
968     * 
969     * @param other the other GeneralizedTime object
970     * 
971     * @return a negative integer, zero, or a positive integer as this object
972     *      is less than, equal to, or greater than the specified object.
973     * 
974     * @see java.lang.Comparable#compareTo(java.lang.Object)
975     */
976    public int compareTo( GeneralizedTime other )
977    {
978        return calendar.compareTo( other.calendar );
979    }
980
981
982    public long getTime()
983    {
984        return calendar.getTimeInMillis();
985    }
986
987
988    public Date getDate()
989    {
990        return calendar.getTime();
991    }
992
993
994    public int getYear()
995    {
996        return calendar.get( Calendar.YEAR );
997    }
998
999
1000    public int getMonth()
1001    {
1002        return calendar.get( Calendar.MONTH );
1003    }
1004
1005
1006    public int getDay()
1007    {
1008        return calendar.get( Calendar.DATE );
1009    }
1010
1011
1012    public int getHour()
1013    {
1014        return calendar.get( Calendar.HOUR_OF_DAY );
1015    }
1016
1017
1018    public int getMinutes()
1019    {
1020        return calendar.get( Calendar.MINUTE );
1021    }
1022
1023
1024    public int getSeconds()
1025    {
1026        return calendar.get( Calendar.SECOND );
1027    }
1028
1029
1030    public int getFraction()
1031    {
1032        return calendar.get( Calendar.MILLISECOND );
1033    }
1034
1035
1036    /**
1037     * 
1038     *
1039     * @param zuluTime
1040     * @return
1041     */
1042    public static Date getDate( String zuluTime ) throws ParseException
1043    {
1044        return new GeneralizedTime( zuluTime ).calendar.getTime();
1045    }
1046}