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