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     */
020    package org.apache.directory.shared.ldap.util;
021    
022    
023    import java.text.DecimalFormat;
024    import java.text.NumberFormat;
025    import java.text.ParseException;
026    import java.util.Calendar;
027    import java.util.TimeZone;
028    
029    
030    /**
031     * <p>This class represents the generalized time syntax as defined in 
032     * RFC 4517 section 3.3.13.</p>
033     * 
034     * <p>The date, time and time zone information is internally backed
035     * by an {@link java.util.Calendar} object</p>
036     * 
037     * <p>Leap seconds are not supported, as {@link java.util.Calendar}
038     * does not support leap seconds.</p>
039     * 
040     * <pre>
041     * 3.3.13.  Generalized Time
042     *
043     *  A value of the Generalized Time syntax is a character string
044     *  representing a date and time.  The LDAP-specific encoding of a value
045     *  of this syntax is a restriction of the format defined in [ISO8601],
046     *  and is described by the following ABNF:
047     *
048     *     GeneralizedTime = century year month day hour
049     *                          [ minute [ second / leap-second ] ]
050     *                          [ fraction ]
051     *                          g-time-zone
052     *
053     *     century = 2(%x30-39) ; "00" to "99"
054     *     year    = 2(%x30-39) ; "00" to "99"
055     *     month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
056     *               / ( %x31 %x30-32 ) ; "10" to "12"
057     *     day     =   ( %x30 %x31-39 )    ; "01" to "09"
058     *               / ( %x31-32 %x30-39 ) ; "10" to "29"
059     *               / ( %x33 %x30-31 )    ; "30" to "31"
060     *     hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
061     *     minute  = %x30-35 %x30-39                        ; "00" to "59"
062     *
063     *     second      = ( %x30-35 %x30-39 ) ; "00" to "59"
064     *     leap-second = ( %x36 %x30 )       ; "60"
065     *
066     *     fraction        = ( DOT / COMMA ) 1*(%x30-39)
067     *     g-time-zone     = %x5A  ; "Z"
068     *                       / g-differential
069     *     g-differential  = ( MINUS / PLUS ) hour [ minute ]
070     *     MINUS           = %x2D  ; minus sign ("-")
071     *
072     *  The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
073     *
074     *  The above ABNF allows character strings that do not represent valid
075     *  dates (in the Gregorian calendar) and/or valid times (e.g., February
076     *  31, 1994).  Such character strings SHOULD be considered invalid for
077     *  this syntax.
078     *
079     *  The time value represents coordinated universal time (equivalent to
080     *  Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
081     *  otherwise, the value represents a local time in the time zone
082     *  indicated by <g-differential>.  In the latter case, coordinated
083     *  universal time can be calculated by subtracting the differential from
084     *  the local time.  The "Z" form of <g-time-zone> SHOULD be used in
085     *  preference to <g-differential>.
086     *
087     *  If <minute> is omitted, then <fraction> represents a fraction of an
088     *  hour; otherwise, if <second> and <leap-second> are omitted, then
089     *  <fraction> represents a fraction of a minute; otherwise, <fraction>
090     *  represents a fraction of a second.
091     *
092     *     Examples:
093     *        199412161032Z
094     *        199412160532-0500
095     *
096     *  Both example values represent the same coordinated universal time:
097     *  10:32 AM, December 16, 1994.
098     *
099     *  The LDAP definition for the Generalized Time syntax is:
100     *
101     *     ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
102     *
103     *  This syntax corresponds to the GeneralizedTime ASN.1 type from
104     *  [ASN.1], with the constraint that local time without a differential
105     *  SHALL NOT be used.
106     *
107     * </pre>
108     */
109    public class GeneralizedTime implements Comparable<GeneralizedTime>
110    {
111    
112        public enum Format
113        {
114            YEAR_MONTH_DAY_HOUR_MIN_SEC, YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
115    
116            YEAR_MONTH_DAY_HOUR_MIN, YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
117    
118            YEAR_MONTH_DAY_HOUR, YEAR_MONTH_DAY_HOUR_FRACTION, ;
119        }
120    
121        public enum FractionDelimiter
122        {
123            DOT, COMMA
124        }
125    
126        public enum TimeZoneFormat
127        {
128            Z, DIFF_HOUR, DIFF_HOUR_MINUTE;
129        }
130    
131        private static final TimeZone GMT = TimeZone.getTimeZone( "GMT" );
132    
133        /** The user provided value */
134        private String upGeneralizedTime;
135    
136        /** The user provided format */
137        private Format upFormat;
138    
139        /** The user provided time zone format */
140        private TimeZoneFormat upTimeZoneFormat;
141    
142        /** The user provided fraction delimiter */
143        private FractionDelimiter upFractionDelimiter;
144    
145        /** the user provided fraction length */
146        private int upFractionLength;
147    
148        /** The calendar */
149        private Calendar calendar;
150    
151    
152        /**
153         * Creates a new instance of GeneralizedTime, based on the given Calendar object.
154         * Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
155         * <pre>TimeZoneFormat.Z</pre> as default time zone format. 
156         *
157         * @param calendar the calendar containing the date, time and timezone information
158         */
159        public GeneralizedTime( Calendar calendar )
160        {
161            if ( calendar == null )
162            {
163                throw new IllegalArgumentException( "Calendar must not be null." );
164            }
165    
166            this.calendar = calendar;
167            upGeneralizedTime = null;
168            upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
169            upTimeZoneFormat = TimeZoneFormat.Z;
170            upFractionDelimiter = FractionDelimiter.DOT;
171            upFractionLength = 3;
172        }
173    
174    
175        /**
176         * Creates a new instance of GeneralizedTime, based on the
177         * given generalized time string.
178         *
179         * @param generalizedTime the generalized time
180         * 
181         * @throws ParseException if the given generalized time can't be parsed.
182         */
183        public GeneralizedTime( String generalizedTime ) throws ParseException
184        {
185            if ( generalizedTime == null )
186            {
187                throw new ParseException( "generalizedTime is null", 0 );
188            }
189    
190            this.upGeneralizedTime = generalizedTime;
191    
192            calendar = Calendar.getInstance();
193            calendar.setTimeInMillis( 0 );
194            calendar.setLenient( false );
195    
196            parseYear();
197            parseMonth();
198            parseDay();
199            parseHour();
200    
201            if ( upGeneralizedTime.length() < 11 )
202            {
203                throw new ParseException(
204                    "Generalized Time too short, doesn't contain field 'minute' or 'fraction of hour' or 'timezone'.", 10 );
205            }
206    
207            // pos 10: 
208            // if digit => minute field
209            // if . or , => fraction of hour field
210            // if Z or + or - => timezone field
211            // else error
212            int pos = 10;
213            char c = upGeneralizedTime.charAt( pos );
214            if ( '0' <= c && c <= '9' )
215            {
216                parseMinute();
217    
218                if ( upGeneralizedTime.length() < 13 )
219                {
220                    throw new ParseException(
221                        "Generalized Time too short, doesn't contain field 'second' or 'fraction of minute' or 'timezone'.",
222                        12 );
223                }
224    
225                // pos 12: 
226                // if digit => second field
227                // if . or , => fraction of minute field
228                // if Z or + or - => timezone field
229                // else error
230                pos = 12;
231                c = upGeneralizedTime.charAt( pos );
232                if ( '0' <= c && c <= '9' )
233                {
234                    parseSecond();
235    
236                    if ( upGeneralizedTime.length() < 15 )
237                    {
238                        throw new ParseException(
239                            "Generalized Time too short, doesn't contain field 'fraction of second' or 'timezone'.", 14 );
240                    }
241    
242                    // pos 14: 
243                    // if . or , => fraction of second field
244                    // if Z or + or - => timezone field
245                    // else error
246                    pos = 14;
247                    c = upGeneralizedTime.charAt( pos );
248                    if ( c == '.' || c == ',' )
249                    {
250                        // read fraction of second
251                        parseFractionOfSecond();
252                        pos += 1 + upFractionLength;
253    
254                        parseTimezone( pos );
255                        upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
256                    }
257                    else if ( c == 'Z' || c == '+' || c == '-' )
258                    {
259                        // read timezone
260                        parseTimezone( pos );
261                        upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
262                    }
263                    else
264                    {
265                        throw new ParseException(
266                            "Invalid Time too short, expected field 'fraction of second' or 'timezone'.", 14 );
267                    }
268                }
269                else if ( c == '.' || c == ',' )
270                {
271                    // read fraction of minute
272                    parseFractionOfMinute();
273                    pos += 1 + upFractionLength;
274    
275                    parseTimezone( pos );
276                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
277                }
278                else if ( c == 'Z' || c == '+' || c == '-' )
279                {
280                    // read timezone
281                    parseTimezone( pos );
282                    upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
283                }
284                else
285                {
286                    throw new ParseException(
287                        "Invalid Time too short, expected field 'second' or 'fraction of minute' or 'timezone'.", 12 );
288                }
289            }
290            else if ( c == '.' || c == ',' )
291            {
292                // read fraction of hour
293                parseFractionOfHour();
294                pos += 1 + upFractionLength;
295    
296                parseTimezone( pos );
297                upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
298            }
299            else if ( c == 'Z' || c == '+' || c == '-' )
300            {
301                // read timezone
302                parseTimezone( pos );
303                upFormat = Format.YEAR_MONTH_DAY_HOUR;
304            }
305            else
306            {
307                throw new ParseException(
308                    "Invalid Generalized Time, expected field 'minute' or 'fraction of hour' or 'timezone'.", 10 );
309            }
310    
311            // this calculates and verifies the calendar
312            try
313            {
314                calendar.getTimeInMillis();
315            }
316            catch ( IllegalArgumentException iae )
317            {
318                throw new ParseException( "Invalid date/time values.", 0 );
319            }
320    
321            calendar.setLenient( true );
322        }
323    
324    
325        private void parseTimezone( int pos ) throws ParseException
326        {
327            if ( upGeneralizedTime.length() < pos + 1 )
328            {
329                throw new ParseException( "Generalized Time too short, doesn't contain field 'timezone'.", pos );
330            }
331    
332            char c = upGeneralizedTime.charAt( pos );
333            if ( c == 'Z' )
334            {
335                calendar.setTimeZone( GMT );
336                upTimeZoneFormat = TimeZoneFormat.Z;
337    
338                if ( upGeneralizedTime.length() > pos + 1 )
339                {
340                    throw new ParseException( "Invalid Generalized Time, expected 'timezone' as the last field.", pos + 1 );
341                }
342            }
343            else if ( c == '+' || c == '-' )
344            {
345                StringBuilder sb = new StringBuilder( "GMT" );
346                sb.append( c );
347    
348                String digits = getAllDigits( pos + 1 );
349                sb.append( digits );
350    
351                if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
352                {
353                    TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
354                    calendar.setTimeZone( timeZone );
355                    upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
356                }
357                else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
358                {
359                    TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
360                    calendar.setTimeZone( timeZone );
361                    upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
362                }
363                else
364                {
365                    throw new ParseException(
366                        "Invalid Generalized Time, expected field 'timezone' must contain 2 or 4 digits.", pos );
367                }
368    
369                if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
370                {
371                    throw new ParseException( "Invalid Generalized Time, expected 'timezone' as the last field.", pos + 1
372                        + digits.length() );
373                }
374            }
375        }
376    
377    
378        private void parseFractionOfSecond() throws ParseException
379        {
380            parseFractionDelmiter( 14 );
381            String fraction = getFraction( 14 + 1 );
382            upFractionLength = fraction.length();
383    
384            double fract = Double.parseDouble( "0." + fraction );
385            int millisecond = ( int ) Math.round( fract * 1000 );
386    
387            calendar.set( Calendar.MILLISECOND, millisecond );
388        }
389    
390    
391        private void parseFractionOfMinute() throws ParseException
392        {
393            parseFractionDelmiter( 12 );
394            String fraction = getFraction( 12 + 1 );
395            upFractionLength = fraction.length();
396    
397            double fract = Double.parseDouble( "0." + fraction );
398            int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
399            int second = milliseconds / 1000;
400            int millisecond = milliseconds - ( second * 1000 );
401    
402            calendar.set( Calendar.SECOND, second );
403            calendar.set( Calendar.MILLISECOND, millisecond );
404        }
405    
406    
407        private void parseFractionOfHour() throws ParseException
408        {
409            parseFractionDelmiter( 10 );
410            String fraction = getFraction( 10 + 1 );
411            upFractionLength = fraction.length();
412    
413            double fract = Double.parseDouble( "0." + fraction );
414            int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
415            int minute = milliseconds / ( 1000 * 60 );
416            int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
417            int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
418    
419            calendar.set( Calendar.MINUTE, minute );
420            calendar.set( Calendar.SECOND, second );
421            calendar.set( Calendar.MILLISECOND, millisecond );
422        }
423    
424    
425        private void parseFractionDelmiter( int fractionDelimiterPos )
426        {
427            char c = upGeneralizedTime.charAt( fractionDelimiterPos );
428            upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
429        }
430    
431    
432        private String getFraction( int startIndex ) throws ParseException
433        {
434            String fraction = getAllDigits( startIndex );
435    
436            // minimum one digit
437            if ( fraction.length() == 0 )
438            {
439                throw new ParseException( "Generalized Time too short, doesn't contain number for 'fraction'.", startIndex );
440            }
441    
442            return fraction;
443        }
444    
445    
446        private String getAllDigits( int startIndex )
447        {
448            StringBuilder sb = new StringBuilder();
449            while ( upGeneralizedTime.length() > startIndex )
450            {
451                char c = upGeneralizedTime.charAt( startIndex );
452                if ( '0' <= c && c <= '9' )
453                {
454                    sb.append( c );
455                    startIndex++;
456                }
457                else
458                {
459                    break;
460                }
461            }
462            return sb.toString();
463        }
464    
465    
466        private void parseSecond() throws ParseException
467        {
468            // read minute
469            if ( upGeneralizedTime.length() < 14 )
470            {
471                throw new ParseException( "Generalized Time too short, doesn't contain field 'second'.", 12 );
472            }
473            try
474            {
475                int second = Integer.parseInt( upGeneralizedTime.substring( 12, 14 ) );
476                calendar.set( Calendar.SECOND, second );
477            }
478            catch ( NumberFormatException e )
479            {
480                throw new ParseException( "Invalid Generalized Time, field 'second' is not numeric.", 12 );
481            }
482        }
483    
484    
485        private void parseMinute() throws ParseException
486        {
487            // read minute
488            if ( upGeneralizedTime.length() < 12 )
489            {
490                throw new ParseException( "Generalized Time too short, doesn't contain field 'minute'.", 10 );
491            }
492            try
493            {
494                int minute = Integer.parseInt( upGeneralizedTime.substring( 10, 12 ) );
495                calendar.set( Calendar.MINUTE, minute );
496            }
497            catch ( NumberFormatException e )
498            {
499                throw new ParseException( "Invalid Generalized Time, field 'minute' is not numeric.", 10 );
500            }
501        }
502    
503    
504        private void parseHour() throws ParseException
505        {
506            if ( upGeneralizedTime.length() < 10 )
507            {
508                throw new ParseException( "Generalized Time too short, doesn't contain field 'hour'.", 8 );
509            }
510            try
511            {
512                int hour = Integer.parseInt( upGeneralizedTime.substring( 8, 10 ) );
513                calendar.set( Calendar.HOUR_OF_DAY, hour );
514            }
515            catch ( NumberFormatException e )
516            {
517                throw new ParseException( "Invalid Generalized Time, field 'hour' is not numeric.", 8 );
518            }
519        }
520    
521    
522        private void parseDay() throws ParseException
523        {
524            if ( upGeneralizedTime.length() < 8 )
525            {
526                throw new ParseException( "Generalized Time too short, doesn't contain field 'day'.", 6 );
527            }
528            try
529            {
530                int day = Integer.parseInt( upGeneralizedTime.substring( 6, 8 ) );
531                calendar.set( Calendar.DAY_OF_MONTH, day );
532            }
533            catch ( NumberFormatException e )
534            {
535                throw new ParseException( "Invalid Generalized Time, field 'day' is not numeric.", 6 );
536            }
537        }
538    
539    
540        private void parseMonth() throws ParseException
541        {
542            if ( upGeneralizedTime.length() < 6 )
543            {
544                throw new ParseException( "Generalized Time too short, doesn't contain field 'month'.", 4 );
545            }
546            try
547            {
548                int month = Integer.parseInt( upGeneralizedTime.substring( 4, 6 ) );
549                calendar.set( Calendar.MONTH, month - 1 );
550            }
551            catch ( NumberFormatException e )
552            {
553                throw new ParseException( "Invalid Generalized Time, field 'month' is not numeric.", 4 );
554            }
555        }
556    
557    
558        private void parseYear() throws ParseException
559        {
560            if ( upGeneralizedTime.length() < 4 )
561            {
562                throw new ParseException( "Generalized Time too short, doesn't contain field 'century/year'.", 0 );
563            }
564            try
565            {
566                int year = Integer.parseInt( upGeneralizedTime.substring( 0, 4 ) );
567                calendar.set( Calendar.YEAR, year );
568            }
569            catch ( NumberFormatException e )
570            {
571                throw new ParseException( "Invalid Generalized Time, field 'century/year' is not numeric.", 0 );
572            }
573        }
574    
575    
576        /**
577         * Returns the string representation of this generalized time. 
578         * This method uses the same format as the user provided format.
579         *
580         * @return the string representation of this generalized time
581         */
582        public String toGeneralizedTime()
583        {
584            return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
585        }
586    
587    
588        /**
589         * Returns the string representation of this generalized time.
590         * 
591         * @param format the target format
592         * @param fractionDelimiter the target fraction delimiter, may be null
593         * @param fractionLenth the fraction length
594         * @param timeZoneFormat the target time zone format
595         * 
596         * @return the string
597         */
598        public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
599            TimeZoneFormat timeZoneFormat )
600        {
601            NumberFormat twoDigits = new DecimalFormat( "00" );
602            NumberFormat fourDigits = new DecimalFormat( "00" );
603            String fractionFormat = "";
604            for ( int i = 0; i < fractionLength && i < 3; i++ )
605            {
606                fractionFormat += "0";
607            }
608    
609            StringBuilder sb = new StringBuilder();
610            sb.append( fourDigits.format( calendar.get( Calendar.YEAR ) ) );
611            sb.append( twoDigits.format( calendar.get( Calendar.MONTH ) + 1 ) );
612            sb.append( twoDigits.format( calendar.get( Calendar.DAY_OF_MONTH ) ) );
613            sb.append( twoDigits.format( calendar.get( Calendar.HOUR_OF_DAY ) ) );
614    
615            switch ( format )
616            {
617                case YEAR_MONTH_DAY_HOUR_MIN_SEC:
618                    sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
619                    sb.append( twoDigits.format( calendar.get( Calendar.SECOND ) ) );
620                    break;
621    
622                case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
623                    sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
624                    sb.append( twoDigits.format( calendar.get( Calendar.SECOND ) ) );
625    
626                    NumberFormat fractionDigits = new DecimalFormat( fractionFormat );
627                    sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
628                    sb.append( fractionDigits.format( calendar.get( Calendar.MILLISECOND ) ) );
629                    break;
630    
631                case YEAR_MONTH_DAY_HOUR_MIN:
632                    sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
633                    break;
634    
635                case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
636                    sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
637    
638                    // sec + millis => fraction of minute
639                    double millisec = 1000 * calendar.get( Calendar.SECOND ) + calendar.get( Calendar.MILLISECOND );
640                    double fraction = millisec / ( 1000 * 60 );
641                    fractionDigits = new DecimalFormat( "0." + fractionFormat );
642                    sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
643                    sb.append( fractionDigits.format( fraction ).substring( 2 ) );
644                    break;
645    
646                case YEAR_MONTH_DAY_HOUR_FRACTION:
647                    // min + sec + millis => fraction of minute
648                    millisec = 1000 * 60 * calendar.get( Calendar.MINUTE ) + 1000 * calendar.get( Calendar.SECOND )
649                        + calendar.get( Calendar.MILLISECOND );
650                    fraction = millisec / ( 1000 * 60 * 60 );
651                    fractionDigits = new DecimalFormat( "0." + fractionFormat );
652                    sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
653                    sb.append( fractionDigits.format( fraction ).substring( 2 ) );
654    
655                    break;
656            }
657    
658            if ( timeZoneFormat == TimeZoneFormat.Z && calendar.getTimeZone().hasSameRules( GMT ) )
659            {
660                sb.append( 'Z' );
661            }
662            else
663            {
664                TimeZone timeZone = calendar.getTimeZone();
665                int rawOffset = timeZone.getRawOffset();
666                sb.append( rawOffset < 0 ? '-' : '+' );
667    
668                rawOffset = Math.abs( rawOffset );
669                int hour = rawOffset / ( 60 * 60 * 1000 );
670                int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
671    
672                if ( hour < 10 )
673                {
674                    sb.append( '0' );
675                }
676                sb.append( hour );
677    
678                if ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE || timeZoneFormat == TimeZoneFormat.Z )
679                {
680                    if ( minute < 10 )
681                    {
682                        sb.append( '0' );
683                    }
684                    sb.append( minute );
685                }
686            }
687    
688            return sb.toString();
689        }
690    
691    
692        /**
693         * Gets the calendar. It could be used to manipulate this 
694         * {@link GeneralizedTime} settings.
695         * 
696         * @return the calendar
697         */
698        public Calendar getCalendar()
699        {
700            return calendar;
701        }
702    
703    
704        @Override
705        public String toString()
706        {
707            return toGeneralizedTime();
708        }
709    
710    
711        @Override
712        public int hashCode()
713        {
714            final int prime = 31;
715            int result = 1;
716            result = prime * result + calendar.hashCode();
717            return result;
718        }
719    
720    
721        @Override
722        public boolean equals( Object obj )
723        {
724            if ( obj instanceof GeneralizedTime )
725            {
726                GeneralizedTime other = ( GeneralizedTime ) obj;
727                return calendar.equals( other.calendar );
728            }
729            else
730            {
731                return false;
732            }
733        }
734    
735    
736        /**
737         * Compares this GeneralizedTime object with the specified GeneralizedTime object.
738         * 
739         * @param other the other GeneralizedTime object
740         * 
741         * @return a negative integer, zero, or a positive integer as this object
742         *      is less than, equal to, or greater than the specified object.
743         * 
744         * @see java.lang.Comparable#compareTo(java.lang.Object)
745         */
746        public int compareTo( GeneralizedTime other )
747        {
748            return calendar.compareTo( other.calendar );
749        }
750    
751    }