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