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
022import static org.apache.directory.api.util.TimeZones.GMT;
023
024import java.text.ParseException;
025import java.util.Calendar;
026import java.util.Date;
027import java.util.GregorianCalendar;
028import java.util.Locale;
029import java.util.TimeZone;
030
031import org.apache.directory.api.i18n.I18n;
032
033
034/**
035 * <p>This class represents the generalized time syntax as defined in 
036 * RFC 4517 section 3.3.13.</p>
037 * 
038 * <p>The date, time and time zone information is internally backed
039 * by an {@link java.util.Calendar} object</p>
040 * 
041 * <p>Leap seconds are not supported, as {@link java.util.Calendar}
042 * does not support leap seconds.</p>
043 * 
044 * <pre>
045 * 3.3.13.  Generalized Time
046 *
047 *  A value of the Generalized Time syntax is a character string
048 *  representing a date and time.  The LDAP-specific encoding of a value
049 *  of this syntax is a restriction of the format defined in [ISO8601],
050 *  and is described by the following ABNF:
051 *
052 *     GeneralizedTime = century year month day hour
053 *                          [ minute [ second / leap-second ] ]
054 *                          [ fraction ]
055 *                          g-time-zone
056 *
057 *     century = 2(%x30-39) ; "00" to "99"
058 *     year    = 2(%x30-39) ; "00" to "99"
059 *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
060 *               / ( %x31 %x30-32 ) ; "10" to "12"
061 *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
062 *               / ( %x31-32 %x30-39 ) ; "10" to "29"
063 *               / ( %x33 %x30-31 )    ; "30" to "31"
064 *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
065 *     minute  = %x30-35 %x30-39                        ; "00" to "59"
066 *
067 *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
068 *     leap-second = ( %x36 %x30 )       ; "60"
069 *
070 *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
071 *     g-time-zone     = %x5A  ; "Z"
072 *                       / g-differential
073 *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
074 *     MINUS           = %x2D  ; minus sign ("-")
075 *
076 *  The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
077 *
078 *  The above ABNF allows character strings that do not represent valid
079 *  dates (in the Gregorian calendar) and/or valid times (e.g., February
080 *  31, 1994).  Such character strings SHOULD be considered invalid for
081 *  this syntax.
082 *
083 *  The time value represents coordinated universal time (equivalent to
084 *  Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
085 *  otherwise, the value represents a local time in the time zone
086 *  indicated by <g-differential>.  In the latter case, coordinated
087 *  universal time can be calculated by subtracting the differential from
088 *  the local time.  The "Z" form of <g-time-zone> SHOULD be used in
089 *  preference to <g-differential>.
090 *
091 *  If <minute> is omitted, then <fraction> represents a fraction of an
092 *  hour; otherwise, if <second> and <leap-second> are omitted, then
093 *  <fraction> represents a fraction of a minute; otherwise, <fraction>
094 *  represents a fraction of a second.
095 *
096 *     Examples:
097 *        199412161032Z
098 *        199412160532-0500
099 *
100 *  Both example values represent the same coordinated universal time:
101 *  10:32 AM, December 16, 1994.
102 *
103 *  The LDAP definition for the Generalized Time syntax is:
104 *
105 *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
106 *
107 *  This syntax corresponds to the GeneralizedTime ASN.1 type from
108 *  [ASN.1], with the constraint that local time without a differential
109 *  SHALL NOT be used.
110 *
111 * </pre>
112 */
113public class GeneralizedTime implements Comparable<GeneralizedTime>
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    /** The user provided value */
161    private String upGeneralizedTime;
162
163    /** The user provided format */
164    private Format upFormat;
165
166    /** The user provided time zone format */
167    private TimeZoneFormat upTimeZoneFormat;
168
169    /** The user provided fraction delimiter */
170    private FractionDelimiter upFractionDelimiter;
171
172    /** the user provided fraction length */
173    private int upFractionLength;
174
175    /** The calendar */
176    private Calendar calendar;
177
178
179    /**
180     * 
181     * Creates a new instance of GeneralizedTime by setting the date to an instance of Calendar.
182     * @see #GeneralizedTime(Calendar)
183     * 
184     * @param date the date
185     */
186    public GeneralizedTime( Date date )
187    {
188        calendar = new GregorianCalendar( GMT, Locale.ROOT );
189        calendar.setTime( date );
190        setUp( calendar );
191    }
192
193
194    /**
195     * Creates a new instance of GeneralizedTime, based on the given Calendar object.
196     * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
197     * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
198     *
199     * @param calendar the calendar containing the date, time and timezone information
200     */
201    public GeneralizedTime( Calendar calendar )
202    {
203        setUp( calendar );
204    }
205
206
207    private void setUp( Calendar newCalendar )
208    {
209        if ( newCalendar == null )
210        {
211            throw new IllegalArgumentException( I18n.err( I18n.ERR_04358 ) );
212        }
213
214        this.calendar = newCalendar;
215        upGeneralizedTime = null;
216        upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
217        upTimeZoneFormat = TimeZoneFormat.Z;
218        upFractionDelimiter = FractionDelimiter.DOT;
219        upFractionLength = 3;
220    }
221
222
223    /**
224     * Creates a new instance of GeneralizedTime, based on the
225     * given generalized time string.
226     *
227     * @param generalizedTime the generalized time
228     * 
229     * @throws ParseException if the given generalized time can't be parsed.
230     */
231    public GeneralizedTime( String generalizedTime ) throws ParseException
232    {
233        if ( generalizedTime == null )
234        {
235            throw new ParseException( I18n.err( I18n.ERR_04359 ), 0 );
236        }
237
238        this.upGeneralizedTime = generalizedTime;
239
240        calendar = new GregorianCalendar( GMT, Locale.ROOT );
241        calendar.setTimeInMillis( 0 );
242        calendar.setLenient( false );
243
244        parseYear();
245        parseMonth();
246        parseDay();
247        parseHour();
248
249        if ( upGeneralizedTime.length() < 11 )
250        {
251            throw new ParseException( I18n.err( I18n.ERR_04360 ), 10 );
252        }
253
254        // pos 10: 
255        // if digit => minute field
256        // if . or , => fraction of hour field
257        // if Z or + or - => timezone field
258        // else error
259        int pos = 10;
260        char c = upGeneralizedTime.charAt( pos );
261        
262        if ( ( '0' <= c ) && ( c <= '9' ) )
263        {
264            parseMinute();
265
266            if ( upGeneralizedTime.length() < 13 )
267            {
268                throw new ParseException( I18n.err( I18n.ERR_04361 ), 12 );
269            }
270
271            // pos 12: 
272            // if digit => second field
273            // if . or , => fraction of minute field
274            // if Z or + or - => timezone field
275            // else error
276            pos = 12;
277            c = upGeneralizedTime.charAt( pos );
278            
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                
295                if ( ( c == '.' ) || ( c == ',' ) )
296                {
297                    // read fraction of second
298                    parseFractionOfSecond();
299                    pos += 1 + upFractionLength;
300
301                    parseTimezone( pos );
302                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
303                }
304                else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
305                {
306                    // read timezone
307                    parseTimezone( pos );
308                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
309                }
310                else
311                {
312                    throw new ParseException( I18n.err( I18n.ERR_04363 ), 14 );
313                }
314            }
315            else if ( ( c == '.' ) || ( c == ',' ) )
316            {
317                // read fraction of minute
318                parseFractionOfMinute();
319                pos += 1 + upFractionLength;
320
321                parseTimezone( pos );
322                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
323            }
324            else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
325            {
326                // read timezone
327                parseTimezone( pos );
328                upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
329            }
330            else
331            {
332                throw new ParseException( I18n.err( I18n.ERR_04364 ), 12 );
333            }
334        }
335        else if ( ( c == '.' ) || ( c == ',' ) )
336        {
337            // read fraction of hour
338            parseFractionOfHour();
339            pos += 1 + upFractionLength;
340
341            parseTimezone( pos );
342            upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
343        }
344        else if ( ( c == 'Z' ) || ( c == '+' ) || ( c == '-' ) )
345        {
346            // read timezone
347            parseTimezone( pos );
348            upFormat = Format.YEAR_MONTH_DAY_HOUR;
349        }
350        else
351        {
352            throw new ParseException( I18n.err( I18n.ERR_04365 ), 10 );
353        }
354
355        // this calculates and verifies the calendar
356        /* Not sure we should do that... */
357        try
358        {
359            calendar.getTimeInMillis();
360        }
361        catch ( IllegalArgumentException iae )
362        {
363            throw new ParseException( I18n.err( I18n.ERR_04366 ), 0 );
364        }
365
366        calendar.setLenient( true );
367    }
368
369
370    private void parseTimezone( int pos ) throws ParseException
371    {
372        if ( upGeneralizedTime.length() < pos + 1 )
373        {
374            throw new ParseException( I18n.err( I18n.ERR_04367 ), pos );
375        }
376
377        char c = upGeneralizedTime.charAt( pos );
378        
379        if ( c == 'Z' )
380        {
381            calendar.setTimeZone( GMT );
382            upTimeZoneFormat = TimeZoneFormat.Z;
383
384            if ( upGeneralizedTime.length() > pos + 1 )
385            {
386                throw new ParseException( I18n.err( I18n.ERR_04368 ), pos + 1 );
387            }
388        }
389        else if ( ( c == '+' ) || ( c == '-' ) )
390        {
391            StringBuilder sb = new StringBuilder( "GMT" );
392            sb.append( c );
393
394            String digits = getAllDigits( pos + 1 );
395            sb.append( digits );
396
397            if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
398            {
399                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
400                calendar.setTimeZone( timeZone );
401                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
402            }
403            else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
404            {
405                TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
406                calendar.setTimeZone( timeZone );
407                upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
408            }
409            else
410            {
411                throw new ParseException( I18n.err( I18n.ERR_04369 ), pos );
412            }
413
414            if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
415            {
416                throw new ParseException( I18n.err( I18n.ERR_04370 ), pos + 1 + digits.length() );
417            }
418        }
419    }
420
421
422    private void parseFractionOfSecond() throws ParseException
423    {
424        parseFractionDelmiter( 14 );
425        String fraction = getFraction( 14 + 1 );
426        upFractionLength = fraction.length();
427
428        double fract = Double.parseDouble( "0." + fraction );
429        int millisecond = ( int ) Math.floor( fract * 1000 );
430
431        calendar.set( GregorianCalendar.MILLISECOND, millisecond );
432    }
433
434
435    private void parseFractionOfMinute() throws ParseException
436    {
437        parseFractionDelmiter( 12 );
438        String fraction = getFraction( 12 + 1 );
439        upFractionLength = fraction.length();
440
441        double fract = Double.parseDouble( "0." + fraction );
442        int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
443        int second = milliseconds / 1000;
444        int millisecond = milliseconds - ( second * 1000 );
445
446        calendar.set( Calendar.SECOND, second );
447        calendar.set( Calendar.MILLISECOND, millisecond );
448    }
449
450
451    private void parseFractionOfHour() throws ParseException
452    {
453        parseFractionDelmiter( 10 );
454        String fraction = getFraction( 10 + 1 );
455        upFractionLength = fraction.length();
456
457        double fract = Double.parseDouble( "0." + fraction );
458        int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
459        int minute = milliseconds / ( 1000 * 60 );
460        int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
461        int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
462
463        calendar.set( Calendar.MINUTE, minute );
464        calendar.set( Calendar.SECOND, second );
465        calendar.set( Calendar.MILLISECOND, millisecond );
466    }
467
468
469    private void parseFractionDelmiter( int fractionDelimiterPos )
470    {
471        char c = upGeneralizedTime.charAt( fractionDelimiterPos );
472        upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
473    }
474
475
476    private String getFraction( int startIndex ) throws ParseException
477    {
478        String fraction = getAllDigits( startIndex );
479
480        // minimum one digit
481        if ( fraction.length() == 0 )
482        {
483            throw new ParseException( I18n.err( I18n.ERR_04371 ), startIndex );
484        }
485
486        return fraction;
487    }
488
489
490    private String getAllDigits( int startIndex )
491    {
492        StringBuilder sb = new StringBuilder();
493        while ( upGeneralizedTime.length() > startIndex )
494        {
495            char c = upGeneralizedTime.charAt( startIndex );
496            if ( '0' <= c && c <= '9' )
497            {
498                sb.append( c );
499                startIndex++;
500            }
501            else
502            {
503                break;
504            }
505        }
506        return sb.toString();
507    }
508
509
510    private void parseSecond() throws ParseException
511    {
512        // read minute
513        if ( upGeneralizedTime.length() < 14 )
514        {
515            throw new ParseException( I18n.err( I18n.ERR_04372 ), 12 );
516        }
517        try
518        {
519            int second = Strings.parseInt( upGeneralizedTime.substring( 12, 14 ) );
520            calendar.set( Calendar.SECOND, second );
521        }
522        catch ( NumberFormatException e )
523        {
524            throw new ParseException( I18n.err( I18n.ERR_04373 ), 12 );
525        }
526    }
527
528
529    private void parseMinute() throws ParseException
530    {
531        // read minute
532        if ( upGeneralizedTime.length() < 12 )
533        {
534            throw new ParseException( I18n.err( I18n.ERR_04374 ), 10 );
535        }
536        try
537        {
538            int minute = Strings.parseInt( upGeneralizedTime.substring( 10, 12 ) );
539            calendar.set( Calendar.MINUTE, minute );
540        }
541        catch ( NumberFormatException e )
542        {
543            throw new ParseException( I18n.err( I18n.ERR_04375 ), 10 );
544        }
545    }
546
547
548    private void parseHour() throws ParseException
549    {
550        if ( upGeneralizedTime.length() < 10 )
551        {
552            throw new ParseException( I18n.err( I18n.ERR_04376 ), 8 );
553        }
554        try
555        {
556            int hour = Strings.parseInt( upGeneralizedTime.substring( 8, 10 ) );
557            calendar.set( Calendar.HOUR_OF_DAY, hour );
558        }
559        catch ( NumberFormatException e )
560        {
561            throw new ParseException( I18n.err( I18n.ERR_04377 ), 8 );
562        }
563    }
564
565
566    private void parseDay() throws ParseException
567    {
568        if ( upGeneralizedTime.length() < 8 )
569        {
570            throw new ParseException( I18n.err( I18n.ERR_04378 ), 6 );
571        }
572        try
573        {
574            int day = Strings.parseInt( upGeneralizedTime.substring( 6, 8 ) );
575            calendar.set( Calendar.DAY_OF_MONTH, day );
576        }
577        catch ( NumberFormatException e )
578        {
579            throw new ParseException( I18n.err( I18n.ERR_04379 ), 6 );
580        }
581    }
582
583
584    private void parseMonth() throws ParseException
585    {
586        if ( upGeneralizedTime.length() < 6 )
587        {
588            throw new ParseException( I18n.err( I18n.ERR_04380 ), 4 );
589        }
590        try
591        {
592            int month = Strings.parseInt( upGeneralizedTime.substring( 4, 6 ) );
593            calendar.set( Calendar.MONTH, month - 1 );
594        }
595        catch ( NumberFormatException e )
596        {
597            throw new ParseException( I18n.err( I18n.ERR_04381 ), 4 );
598        }
599    }
600
601
602    private void parseYear() throws ParseException
603    {
604        if ( upGeneralizedTime.length() < 4 )
605        {
606            throw new ParseException( I18n.err( I18n.ERR_04382 ), 0 );
607        }
608        try
609        {
610            int year = Strings.parseInt( upGeneralizedTime.substring( 0, 4 ) );
611            calendar.set( Calendar.YEAR, year );
612        }
613        catch ( NumberFormatException e )
614        {
615            throw new ParseException( I18n.err( I18n.ERR_04383 ), 0 );
616        }
617    }
618
619
620    /**
621     * Returns the string representation of this generalized time. 
622     * This method uses the same format as the user provided format.
623     *
624     * @return the string representation of this generalized time
625     */
626    public String toGeneralizedTime()
627    {
628        return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
629    }
630
631
632    /**
633     * Returns the string representation of this generalized time. 
634     * This method uses the same format as the user provided format.
635     *
636     * @return the string representation of this generalized time
637     */
638    public String toGeneralizedTimeWithoutFraction()
639    {
640        return toGeneralizedTime( getFormatWithoutFraction( upFormat ), upFractionDelimiter, upFractionLength,
641            upTimeZoneFormat );
642    }
643
644
645    /**
646     * Gets the corresponding format with fraction.
647     *
648     * @param f the format
649     * @return the corresponding format without fraction
650     */
651    private Format getFormatWithoutFraction( Format f )
652    {
653        switch ( f )
654        {
655            case YEAR_MONTH_DAY_HOUR_FRACTION:
656                return Format.YEAR_MONTH_DAY_HOUR;
657            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
658                return Format.YEAR_MONTH_DAY_HOUR_MIN;
659            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
660                return Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
661            default:
662                break;
663        }
664
665        return f;
666    }
667
668
669    /**
670     * Returns the string representation of this generalized time.
671     * 
672     * @param format the target format
673     * @param fractionDelimiter the target fraction delimiter, may be null
674     * @param fractionLength the fraction length
675     * @param timeZoneFormat the target time zone format
676     * 
677     * @return the string
678     */
679    public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
680        TimeZoneFormat timeZoneFormat )
681    {
682        Calendar clonedCalendar = ( Calendar ) calendar.clone();
683
684        if ( timeZoneFormat == TimeZoneFormat.Z )
685        {
686            clonedCalendar.setTimeZone( GMT );
687        }
688
689        // Create the result. It can contain a maximum of 23 chars
690        byte[] result = new byte[23];
691
692        // The starting point
693        int pos = 0;
694
695        // Inject the year
696        int year = clonedCalendar.get( Calendar.YEAR );
697
698        result[pos++] = ( byte ) ( ( year / 1000 ) + '0' );
699        year %= 1000;
700
701        result[pos++] = ( byte ) ( ( year / 100 ) + '0' );
702        year %= 100;
703
704        result[pos++] = ( byte ) ( ( year / 10 ) + '0' );
705
706        result[pos++] = ( byte ) ( ( year % 10 ) + '0' );
707
708        // Inject the month
709        int month = clonedCalendar.get( Calendar.MONTH ) + 1;
710
711        result[pos++] = ( byte ) ( ( month / 10 ) + '0' );
712
713        result[pos++] = ( byte ) ( ( month % 10 ) + '0' );
714
715        // Inject the day
716        int day = clonedCalendar.get( Calendar.DAY_OF_MONTH );
717
718        result[pos++] = ( byte ) ( ( day / 10 ) + '0' );
719
720        result[pos++] = ( byte ) ( ( day % 10 ) + '0' );
721
722        // Inject the hour
723        int hour = clonedCalendar.get( Calendar.HOUR_OF_DAY );
724
725        result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
726
727        result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
728
729        switch ( format )
730        {
731            case YEAR_MONTH_DAY_HOUR_MIN_SEC:
732                // Inject the minutes
733                int minute = clonedCalendar.get( Calendar.MINUTE );
734
735                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
736
737                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
738
739                // Inject the seconds
740                int second = clonedCalendar.get( Calendar.SECOND );
741
742                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
743
744                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
745
746                break;
747
748            case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
749                // Inject the minutes
750                minute = clonedCalendar.get( Calendar.MINUTE );
751
752                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
753
754                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
755
756                // Inject the seconds
757                second = clonedCalendar.get( Calendar.SECOND );
758
759                result[pos++] = ( byte ) ( ( second / 10 ) + '0' );
760
761                result[pos++] = ( byte ) ( ( second % 10 ) + '0' );
762
763                // Inject the fraction
764                if ( fractionDelimiter == FractionDelimiter.COMMA )
765                {
766                    result[pos++] = ',';
767                }
768                else
769                {
770                    result[pos++] = '.';
771                }
772
773                // Inject the fraction
774                int millisecond = clonedCalendar.get( Calendar.MILLISECOND );
775
776                result[pos++] = ( byte ) ( ( millisecond / 100 ) + '0' );
777                millisecond %= 100;
778
779                result[pos++] = ( byte ) ( ( millisecond / 10 ) + '0' );
780
781                //if ( millisecond > 0 )
782                result[pos++] = ( byte ) ( ( millisecond % 10 ) + '0' );
783
784                break;
785
786            case YEAR_MONTH_DAY_HOUR_MIN:
787                // Inject the minutes
788                minute = clonedCalendar.get( Calendar.MINUTE );
789
790                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
791
792                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
793                break;
794
795            case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
796                // Inject the minutes
797                minute = clonedCalendar.get( Calendar.MINUTE );
798
799                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
800
801                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
802
803                // sec + millis => fraction of a minute
804                int fraction = 1000 * clonedCalendar.get( Calendar.SECOND )
805                    + clonedCalendar.get( Calendar.MILLISECOND );
806                fraction /= 60;
807
808                if ( fraction > 0 )
809                {
810                    if ( fractionDelimiter == FractionDelimiter.COMMA )
811                    {
812                        result[pos++] = ',';
813                    }
814                    else
815                    {
816                        result[pos++] = '.';
817                    }
818
819                    // At this point, the fraction should be in [999, 1]
820                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
821                    fraction %= 100;
822
823                    if ( fraction > 0 )
824                    {
825                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
826
827                        if ( fraction > 0 )
828                        {
829                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
830                        }
831                    }
832                }
833
834                break;
835
836            case YEAR_MONTH_DAY_HOUR:
837                // nothing to add
838                break;
839
840            case YEAR_MONTH_DAY_HOUR_FRACTION:
841                // min + sec + millis => fraction of an hour
842                fraction = 1000 * 60 * clonedCalendar.get( Calendar.MINUTE ) + 1000
843                    * clonedCalendar.get( Calendar.SECOND )
844                    + clonedCalendar.get( Calendar.MILLISECOND );
845                fraction /= 60 * 60;
846
847                // At this point, the fraction should be in [999, 1]
848                if ( fraction > 0 )
849                {
850                    if ( fractionDelimiter == FractionDelimiter.COMMA )
851                    {
852                        result[pos++] = ',';
853                    }
854                    else
855                    {
856                        result[pos++] = '.';
857                    }
858
859                    result[pos++] = ( byte ) ( ( fraction / 100 ) + '0' );
860                    fraction %= 100;
861
862                    if ( fraction > 0 )
863                    {
864                        result[pos++] = ( byte ) ( ( fraction / 10 ) + '0' );
865
866                        if ( fraction > 0 )
867                        {
868                            result[pos++] = ( byte ) ( ( fraction % 10 ) + '0' );
869                        }
870                    }
871                }
872
873                break;
874
875            default:
876                throw new IllegalArgumentException( "Unexpected format " + format );
877        }
878
879        if ( ( timeZoneFormat == TimeZoneFormat.Z ) && clonedCalendar.getTimeZone().hasSameRules( GMT ) )
880        {
881            result[pos++] = 'Z';
882        }
883        else
884        {
885            // g-differential
886            TimeZone timeZone = clonedCalendar.getTimeZone();
887            int rawOffset = timeZone.getRawOffset();
888
889            if ( rawOffset < 0 )
890            {
891                result[pos++] = '-';
892            }
893            else
894            {
895                result[pos++] = '+';
896            }
897
898            rawOffset = Math.abs( rawOffset );
899            hour = rawOffset / ( 60 * 60 * 1000 );
900            int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
901
902            // The offset hour
903            result[pos++] = ( byte ) ( ( hour / 10 ) + '0' );
904
905            result[pos++] = ( byte ) ( ( hour % 10 ) + '0' );
906
907            if ( ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE ) || ( timeZoneFormat == TimeZoneFormat.Z ) )
908            {
909                // The offset minute
910                result[pos++] = ( byte ) ( ( minute / 10 ) + '0' );
911
912                result[pos++] = ( byte ) ( ( minute % 10 ) + '0' );
913            }
914        }
915
916        return Strings.utf8ToString( result, 0, pos );
917    }
918
919
920    /**
921     * Gets the calendar. It could be used to manipulate this 
922     * {@link GeneralizedTime} settings.
923     * 
924     * @return the calendar
925     */
926    public Calendar getCalendar()
927    {
928        return calendar;
929    }
930
931
932    @Override
933    public String toString()
934    {
935        return toGeneralizedTime();
936    }
937
938
939    @Override
940    public int hashCode()
941    {
942        final int prime = 31;
943        int result = 1;
944        result = prime * result + calendar.hashCode();
945        return result;
946    }
947
948
949    @Override
950    public boolean equals( Object obj )
951    {
952        if ( obj instanceof GeneralizedTime )
953        {
954            GeneralizedTime other = ( GeneralizedTime ) obj;
955            return calendar.equals( other.calendar );
956        }
957        else
958        {
959            return false;
960        }
961    }
962
963
964    /**
965     * Compares this GeneralizedTime object with the specified GeneralizedTime object.
966     * 
967     * @param other the other GeneralizedTime object
968     * 
969     * @return a negative integer, zero, or a positive integer as this object
970     *      is less than, equal to, or greater than the specified object.
971     * 
972     * @see java.lang.Comparable#compareTo(java.lang.Object)
973     */
974    public int compareTo( GeneralizedTime other )
975    {
976        return calendar.compareTo( other.calendar );
977    }
978
979
980    public long getTime()
981    {
982        return calendar.getTimeInMillis();
983    }
984
985
986    public Date getDate()
987    {
988        return calendar.getTime();
989    }
990
991
992    public int getYear()
993    {
994        return calendar.get( Calendar.YEAR );
995    }
996
997
998    public int getMonth()
999    {
1000        return calendar.get( Calendar.MONTH );
1001    }
1002
1003
1004    public int getDay()
1005    {
1006        return calendar.get( Calendar.DATE );
1007    }
1008
1009
1010    public int getHour()
1011    {
1012        return calendar.get( Calendar.HOUR_OF_DAY );
1013    }
1014
1015
1016    public int getMinutes()
1017    {
1018        return calendar.get( Calendar.MINUTE );
1019    }
1020
1021
1022    public int getSeconds()
1023    {
1024        return calendar.get( Calendar.SECOND );
1025    }
1026
1027
1028    public int getFraction()
1029    {
1030        return calendar.get( Calendar.MILLISECOND );
1031    }
1032
1033
1034    /**
1035     * 
1036     *
1037     * @param zuluTime
1038     * @return
1039     */
1040    public static Date getDate( String zuluTime ) throws ParseException
1041    {
1042        return new GeneralizedTime( zuluTime ).calendar.getTime();
1043    }
1044}