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