001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2012-2015 ForgeRock AS.
025 */
026package org.forgerock.opendj.ldap;
027
028import java.util.Calendar;
029import java.util.Date;
030import java.util.GregorianCalendar;
031import java.util.TimeZone;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
035import org.forgerock.i18n.LocalizedIllegalArgumentException;
036import org.forgerock.util.Reject;
037
038import static com.forgerock.opendj.ldap.CoreMessages.*;
039
040/**
041 * An LDAP generalized time as defined in RFC 4517. This class facilitates
042 * parsing of generalized time values to and from {@link Date} and
043 * {@link Calendar} classes.
044 * <p>
045 * The following are examples of generalized time values:
046 *
047 * <pre>
048 * 199412161032Z
049 * 199412160532-0500
050 * </pre>
051 *
052 * @see <a href="http://tools.ietf.org/html/rfc4517#section-3.3.13">RFC 4517 -
053 *      Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching
054 *      Rules </a>
055 */
056public final class GeneralizedTime implements Comparable<GeneralizedTime> {
057
058    /** UTC TimeZone is assumed to never change over JVM lifetime. */
059    private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone("UTC");
060
061    /** The smallest time representable using the generalized time syntax. */
062    public static final GeneralizedTime MIN_GENERALIZED_TIME = valueOf("00010101000000Z");
063
064    /** The smallest time in milli-seconds representable using the generalized time syntax. */
065    public static final long MIN_GENERALIZED_TIME_MS = MIN_GENERALIZED_TIME.getTimeInMillis();
066
067    /**
068     * Returns a generalized time whose value is the current time, using the
069     * default time zone and locale.
070     *
071     * @return A generalized time whose value is the current time.
072     */
073    public static GeneralizedTime currentTime() {
074        return valueOf(Calendar.getInstance());
075    }
076
077    /**
078     * Returns a generalized time representing the provided {@code Calendar}.
079     * <p>
080     * The provided calendar will be defensively copied in order to preserve
081     * immutability.
082     *
083     * @param calendar
084     *            The calendar to be converted to a generalized time.
085     * @return A generalized time representing the provided {@code Calendar}.
086     */
087    public static GeneralizedTime valueOf(final Calendar calendar) {
088        Reject.ifNull(calendar);
089        return new GeneralizedTime((Calendar) calendar.clone(), null, Long.MIN_VALUE, null);
090    }
091
092    /**
093     * Returns a generalized time representing the provided {@code Date}.
094     * <p>
095     * The provided date will be defensively copied in order to preserve
096     * immutability.
097     *
098     * @param date
099     *            The date to be converted to a generalized time.
100     * @return A generalized time representing the provided {@code Date}.
101     */
102    public static GeneralizedTime valueOf(final Date date) {
103        Reject.ifNull(date);
104        return new GeneralizedTime(null, (Date) date.clone(), Long.MIN_VALUE, null);
105    }
106
107    /**
108     * Returns a generalized time representing the provided time in milliseconds
109     * since the epoch.
110     *
111     * @param timeMS
112     *            The time to be converted to a generalized time.
113     * @return A generalized time representing the provided time in milliseconds
114     *         since the epoch.
115     */
116    public static GeneralizedTime valueOf(final long timeMS) {
117        Reject.ifTrue(timeMS < MIN_GENERALIZED_TIME_MS, "timeMS is too old to represent as a generalized time");
118        return new GeneralizedTime(null, null, timeMS, null);
119    }
120
121    /**
122     * Parses the provided string as an LDAP generalized time.
123     *
124     * @param time
125     *            The generalized time value to be parsed.
126     * @return The parsed generalized time.
127     * @throws LocalizedIllegalArgumentException
128     *             If {@code time} cannot be parsed as a valid generalized time
129     *             string.
130     * @throws NullPointerException
131     *             If {@code time} was {@code null}.
132     */
133    public static GeneralizedTime valueOf(final String time) {
134        int year = 0;
135        int month = 0;
136        int day = 0;
137        int hour = 0;
138        int minute = 0;
139        int second = 0;
140
141        // Get the value as a string and verify that it is at least long
142        // enough for "YYYYMMDDhhZ", which is the shortest allowed value.
143        final String valueString = time.toUpperCase();
144        final int length = valueString.length();
145        if (length < 11) {
146            final LocalizableMessage message =
147                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString);
148            throw new LocalizedIllegalArgumentException(message);
149        }
150
151        // The first four characters are the century and year, and they must
152        // be numeric digits between 0 and 9.
153        for (int i = 0; i < 4; i++) {
154            char c = valueString.charAt(i);
155            final int val = toInt(c,
156                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR, valueString, String.valueOf(c));
157            year = (year * 10) + val;
158        }
159
160        // The next two characters are the month, and they must form the
161        // string representation of an integer between 01 and 12.
162        char m1 = valueString.charAt(4);
163        final char m2 = valueString.charAt(5);
164        final String monthValue = valueString.substring(4, 6);
165        switch (m1) {
166        case '0':
167            // m2 must be a digit between 1 and 9.
168            switch (m2) {
169            case '1':
170                month = Calendar.JANUARY;
171                break;
172
173            case '2':
174                month = Calendar.FEBRUARY;
175                break;
176
177            case '3':
178                month = Calendar.MARCH;
179                break;
180
181            case '4':
182                month = Calendar.APRIL;
183                break;
184
185            case '5':
186                month = Calendar.MAY;
187                break;
188
189            case '6':
190                month = Calendar.JUNE;
191                break;
192
193            case '7':
194                month = Calendar.JULY;
195                break;
196
197            case '8':
198                month = Calendar.AUGUST;
199                break;
200
201            case '9':
202                month = Calendar.SEPTEMBER;
203                break;
204
205            default:
206                throw new LocalizedIllegalArgumentException(
207                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
208            }
209            break;
210        case '1':
211            // m2 must be a digit between 0 and 2.
212            switch (m2) {
213            case '0':
214                month = Calendar.OCTOBER;
215                break;
216
217            case '1':
218                month = Calendar.NOVEMBER;
219                break;
220
221            case '2':
222                month = Calendar.DECEMBER;
223                break;
224
225            default:
226                throw new LocalizedIllegalArgumentException(
227                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
228            }
229            break;
230        default:
231            throw new LocalizedIllegalArgumentException(
232                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, monthValue));
233        }
234
235        // The next two characters should be the day of the month, and they
236        // must form the string representation of an integer between 01 and
237        // 31. This doesn't do any validation against the year or month, so
238        // it will allow dates like April 31, or February 29 in a non-leap
239        // year, but we'll let those slide.
240        final char d1 = valueString.charAt(6);
241        final char d2 = valueString.charAt(7);
242        final String dayValue = valueString.substring(6, 8);
243        switch (d1) {
244        case '0':
245            // d2 must be a digit between 1 and 9.
246            day = toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
247            if (day == 0) {
248                throw new LocalizedIllegalArgumentException(
249                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
250            }
251            break;
252
253        case '1':
254            // d2 must be a digit between 0 and 9.
255            day = 10 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
256            break;
257
258        case '2':
259            // d2 must be a digit between 0 and 9.
260            day = 20 + toInt(d2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY, valueString, dayValue);
261            break;
262
263        case '3':
264            // d2 must be either 0 or 1.
265            switch (d2) {
266            case '0':
267                day = 30;
268                break;
269
270            case '1':
271                day = 31;
272                break;
273
274            default:
275                throw new LocalizedIllegalArgumentException(
276                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
277            }
278            break;
279
280        default:
281            throw new LocalizedIllegalArgumentException(
282                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, dayValue));
283        }
284
285        // The next two characters must be the hour, and they must form the
286        // string representation of an integer between 00 and 23.
287        final char h1 = valueString.charAt(8);
288        final char h2 = valueString.charAt(9);
289        final String hourValue = valueString.substring(8, 10);
290        switch (h1) {
291        case '0':
292            hour = toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue);
293            break;
294
295        case '1':
296            hour = 10 + toInt(h2, WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR, valueString, hourValue);
297            break;
298
299        case '2':
300            switch (h2) {
301            case '0':
302                hour = 20;
303                break;
304
305            case '1':
306                hour = 21;
307                break;
308
309            case '2':
310                hour = 22;
311                break;
312
313            case '3':
314                hour = 23;
315                break;
316
317            default:
318                throw new LocalizedIllegalArgumentException(
319                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue));
320            }
321            break;
322
323        default:
324            throw new LocalizedIllegalArgumentException(
325                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, hourValue));
326        }
327
328        // Next, there should be either two digits comprising an integer
329        // between 00 and 59 (for the minute), a letter 'Z' (for the UTC
330        // specifier), a plus or minus sign followed by two or four digits
331        // (for the UTC offset), or a period or comma representing the
332        // fraction.
333        m1 = valueString.charAt(10);
334        switch (m1) {
335        case '0':
336        case '1':
337        case '2':
338        case '3':
339        case '4':
340        case '5':
341            // There must be at least two more characters, and the next one
342            // must be a digit between 0 and 9.
343            if (length < 13) {
344                throw invalidChar(valueString, m1, 10);
345            }
346
347            minute = 10 * (m1 - '0');
348            minute += toInt(valueString.charAt(11),
349                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(10, 12));
350
351            break;
352
353        case 'Z':
354        case 'z':
355                // This is fine only if we are at the end of the value.
356            if (length == 11) {
357                final TimeZone tz = TIME_ZONE_UTC_OBJ;
358                return createTime(valueString, year, month, day, hour, minute, second, tz);
359            } else {
360                throw invalidChar(valueString, m1, 10);
361            }
362
363        case '+':
364        case '-':
365            // These are fine only if there are exactly two or four more
366            // digits that specify a valid offset.
367            if (length == 13 || length == 15) {
368                final TimeZone tz = getTimeZoneForOffset(valueString, 10);
369                return createTime(valueString, year, month, day, hour, minute, second, tz);
370            } else {
371                throw invalidChar(valueString, m1, 10);
372            }
373
374        case '.':
375        case ',':
376            return finishDecodingFraction(valueString, 11, year, month, day, hour, minute, second,
377                    3600000);
378
379        default:
380            throw invalidChar(valueString, m1, 10);
381        }
382
383        // Next, there should be either two digits comprising an integer
384        // between 00 and 60 (for the second, including a possible leap
385        // second), a letter 'Z' (for the UTC specifier), a plus or minus
386        // sign followed by two or four digits (for the UTC offset), or a
387        // period or comma to start the fraction.
388        final char s1 = valueString.charAt(12);
389        switch (s1) {
390        case '0':
391        case '1':
392        case '2':
393        case '3':
394        case '4':
395        case '5':
396            // There must be at least two more characters, and the next one
397            // must be a digit between 0 and 9.
398            if (length < 15) {
399                throw invalidChar(valueString, s1, 12);
400            }
401
402            second = 10 * (s1 - '0');
403            second += toInt(valueString.charAt(13),
404                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE, valueString, valueString.substring(12, 14));
405
406            break;
407
408        case '6':
409            // There must be at least two more characters and the next one
410            // must be a 0.
411            if (length < 15) {
412                throw invalidChar(valueString, s1, 12);
413            }
414
415            if (valueString.charAt(13) != '0') {
416                throw new LocalizedIllegalArgumentException(
417                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get(
418                                valueString, valueString.substring(12, 14)));
419            }
420
421            second = 60;
422            break;
423
424        case 'Z':
425        case 'z':
426            // This is fine only if we are at the end of the value.
427            if (length == 13) {
428                final TimeZone tz = TIME_ZONE_UTC_OBJ;
429                return createTime(valueString, year, month, day, hour, minute, second, tz);
430            } else {
431                throw invalidChar(valueString, s1, 12);
432            }
433
434        case '+':
435        case '-':
436            // These are fine only if there are exactly two or four more
437            // digits that specify a valid offset.
438            if (length == 15 || length == 17) {
439                final TimeZone tz = getTimeZoneForOffset(valueString, 12);
440                return createTime(valueString, year, month, day, hour, minute, second, tz);
441            } else {
442                throw invalidChar(valueString, s1, 12);
443            }
444
445        case '.':
446        case ',':
447            return finishDecodingFraction(valueString, 13, year, month, day, hour, minute, second,
448                    60000);
449
450        default:
451            throw invalidChar(valueString, s1, 12);
452        }
453
454        // Next, there should be either a period or comma followed by
455        // between one and three digits (to specify the sub-second), a
456        // letter 'Z' (for the UTC specifier), or a plus or minus sign
457        // followed by two our four digits (for the UTC offset).
458        switch (valueString.charAt(14)) {
459        case '.':
460        case ',':
461            return finishDecodingFraction(valueString, 15, year, month, day, hour, minute, second,
462                    1000);
463
464        case 'Z':
465        case 'z':
466            // This is fine only if we are at the end of the value.
467            if (length == 15) {
468                final TimeZone tz = TIME_ZONE_UTC_OBJ;
469                return createTime(valueString, year, month, day, hour, minute, second, tz);
470            } else {
471                throw invalidChar(valueString, valueString.charAt(14), 14);
472            }
473
474        case '+':
475        case '-':
476            // These are fine only if there are exactly two or four more
477            // digits that specify a valid offset.
478            if (length == 17 || length == 19) {
479                final TimeZone tz = getTimeZoneForOffset(valueString, 14);
480                return createTime(valueString, year, month, day, hour, minute, second, tz);
481            } else {
482                throw invalidChar(valueString, valueString.charAt(14), 14);
483            }
484
485        default:
486            throw invalidChar(valueString, valueString.charAt(14), 14);
487        }
488    }
489
490    private static LocalizedIllegalArgumentException invalidChar(String valueString, char c, int pos) {
491        return new LocalizedIllegalArgumentException(
492                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
493                        valueString, String.valueOf(c), pos));
494    }
495
496    private static int toInt(char c, Arg2<Object, Object> invalidSyntaxMsg, String valueString, String unitValue) {
497        switch (c) {
498        case '0':
499            return 0;
500        case '1':
501            return 1;
502        case '2':
503            return 2;
504        case '3':
505            return 3;
506        case '4':
507            return 4;
508        case '5':
509            return 5;
510        case '6':
511            return 6;
512        case '7':
513            return 7;
514        case '8':
515            return 8;
516        case '9':
517            return 9;
518        default:
519            throw new LocalizedIllegalArgumentException(
520                invalidSyntaxMsg.get(valueString, unitValue));
521        }
522    }
523
524    /**
525     * Returns a generalized time object representing the provided date / time
526     * parameters.
527     *
528     * @param value
529     *            The generalized time string representation.
530     * @param year
531     *            The year.
532     * @param month
533     *            The month.
534     * @param day
535     *            The day.
536     * @param hour
537     *            The hour.
538     * @param minute
539     *            The minute.
540     * @param second
541     *            The second.
542     * @param tz
543     *            The timezone.
544     * @return A generalized time representing the provided date / time
545     *         parameters.
546     * @throws LocalizedIllegalArgumentException
547     *             If the generalized time could not be created.
548     */
549    private static GeneralizedTime createTime(final String value, final int year, final int month,
550            final int day, final int hour, final int minute, final int second, final TimeZone tz) {
551        try {
552            final GregorianCalendar calendar = new GregorianCalendar();
553            calendar.setLenient(false);
554            calendar.setTimeZone(tz);
555            calendar.set(year, month, day, hour, minute, second);
556            calendar.set(Calendar.MILLISECOND, 0);
557            return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value);
558        } catch (final Exception e) {
559            // This should only happen if the provided date wasn't legal
560            // (e.g., September 31).
561            final LocalizableMessage message =
562                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
563            throw new LocalizedIllegalArgumentException(message, e);
564        }
565    }
566
567    /**
568     * Completes decoding the generalized time value containing a fractional
569     * component. It will also decode the trailing 'Z' or offset.
570     *
571     * @param value
572     *            The whole value, including the fractional component and time
573     *            zone information.
574     * @param startPos
575     *            The position of the first character after the period in the
576     *            value string.
577     * @param year
578     *            The year decoded from the provided value.
579     * @param month
580     *            The month decoded from the provided value.
581     * @param day
582     *            The day decoded from the provided value.
583     * @param hour
584     *            The hour decoded from the provided value.
585     * @param minute
586     *            The minute decoded from the provided value.
587     * @param second
588     *            The second decoded from the provided value.
589     * @param multiplier
590     *            The multiplier value that should be used to scale the fraction
591     *            appropriately. If it's a fraction of an hour, then it should
592     *            be 3600000 (60*60*1000). If it's a fraction of a minute, then
593     *            it should be 60000. If it's a fraction of a second, then it
594     *            should be 1000.
595     * @return The timestamp created from the provided generalized time value
596     *         including the fractional element.
597     * @throws LocalizedIllegalArgumentException
598     *             If the provided value cannot be parsed as a valid generalized
599     *             time string.
600     */
601    private static GeneralizedTime finishDecodingFraction(final String value, final int startPos,
602            final int year, final int month, final int day, final int hour, final int minute,
603            final int second, final int multiplier) {
604        final int length = value.length();
605        final StringBuilder fractionBuffer = new StringBuilder((2 + length) - startPos);
606        fractionBuffer.append("0.");
607
608        TimeZone timeZone = null;
609
610    outerLoop:
611        for (int i = startPos; i < length; i++) {
612            final char c = value.charAt(i);
613            switch (c) {
614            case '0':
615            case '1':
616            case '2':
617            case '3':
618            case '4':
619            case '5':
620            case '6':
621            case '7':
622            case '8':
623            case '9':
624                fractionBuffer.append(c);
625                break;
626
627            case 'Z':
628            case 'z':
629                // This is only acceptable if we're at the end of the value.
630                if (i != (value.length() - 1)) {
631                    final LocalizableMessage message =
632                            WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value,
633                                    String.valueOf(c));
634                    throw new LocalizedIllegalArgumentException(message);
635                }
636
637                timeZone = TIME_ZONE_UTC_OBJ;
638                break outerLoop;
639
640            case '+':
641            case '-':
642                timeZone = getTimeZoneForOffset(value, i);
643                break outerLoop;
644
645            default:
646                final LocalizableMessage message =
647                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value, String
648                                .valueOf(c));
649                throw new LocalizedIllegalArgumentException(message);
650            }
651        }
652
653        if (fractionBuffer.length() == 2) {
654            final LocalizableMessage message =
655                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value);
656            throw new LocalizedIllegalArgumentException(message);
657        }
658
659        if (timeZone == null) {
660            final LocalizableMessage message =
661                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value);
662            throw new LocalizedIllegalArgumentException(message);
663        }
664
665        final Double fractionValue = Double.parseDouble(fractionBuffer.toString());
666        final int additionalMilliseconds = (int) Math.round(fractionValue * multiplier);
667
668        try {
669            final GregorianCalendar calendar = new GregorianCalendar();
670            calendar.setLenient(false);
671            calendar.setTimeZone(timeZone);
672            calendar.set(year, month, day, hour, minute, second);
673            calendar.set(Calendar.MILLISECOND, additionalMilliseconds);
674            return new GeneralizedTime(calendar, null, Long.MIN_VALUE, value);
675        } catch (final Exception e) {
676            // This should only happen if the provided date wasn't legal
677            // (e.g., September 31).
678            final LocalizableMessage message =
679                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
680            throw new LocalizedIllegalArgumentException(message, e);
681        }
682    }
683
684    /**
685     * Decodes a time zone offset from the provided value.
686     *
687     * @param value
688     *            The whole value, including the offset.
689     * @param startPos
690     *            The position of the first character that is contained in the
691     *            offset. This should be the position of the plus or minus
692     *            character.
693     * @return The {@code TimeZone} object representing the decoded time zone.
694     */
695    private static TimeZone getTimeZoneForOffset(final String value, final int startPos) {
696        final String offSetStr = value.substring(startPos);
697        final int len = offSetStr.length();
698        if (len != 3 && len != 5) {
699            final LocalizableMessage message =
700                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
701            throw new LocalizedIllegalArgumentException(message);
702        }
703
704        // The first character must be either a plus or minus.
705        switch (offSetStr.charAt(0)) {
706        case '+':
707        case '-':
708            // These are OK.
709            break;
710
711        default:
712            final LocalizableMessage message =
713                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
714            throw new LocalizedIllegalArgumentException(message);
715        }
716
717        // The first two characters must be an integer between 00 and 23.
718        switch (offSetStr.charAt(1)) {
719        case '0':
720        case '1':
721            switch (offSetStr.charAt(2)) {
722            case '0':
723            case '1':
724            case '2':
725            case '3':
726            case '4':
727            case '5':
728            case '6':
729            case '7':
730            case '8':
731            case '9':
732                // These are all fine.
733                break;
734
735            default:
736                final LocalizableMessage message =
737                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
738                throw new LocalizedIllegalArgumentException(message);
739            }
740            break;
741
742        case '2':
743            switch (offSetStr.charAt(2)) {
744            case '0':
745            case '1':
746            case '2':
747            case '3':
748                // These are all fine.
749                break;
750
751            default:
752                final LocalizableMessage message =
753                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
754                throw new LocalizedIllegalArgumentException(message);
755            }
756            break;
757
758        default:
759            final LocalizableMessage message =
760                    WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
761            throw new LocalizedIllegalArgumentException(message);
762        }
763
764        // If there are two more characters, then they must be an integer
765        // between 00 and 59.
766        if (offSetStr.length() == 5) {
767            switch (offSetStr.charAt(3)) {
768            case '0':
769            case '1':
770            case '2':
771            case '3':
772            case '4':
773            case '5':
774                switch (offSetStr.charAt(4)) {
775                case '0':
776                case '1':
777                case '2':
778                case '3':
779                case '4':
780                case '5':
781                case '6':
782                case '7':
783                case '8':
784                case '9':
785                    // These are all fine.
786                    break;
787
788                default:
789                    final LocalizableMessage message =
790                            WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
791                    throw new LocalizedIllegalArgumentException(message);
792                }
793                break;
794
795            default:
796                final LocalizableMessage message =
797                        WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
798                throw new LocalizedIllegalArgumentException(message);
799            }
800        }
801
802        // If we've gotten here, then it looks like a valid offset. We can
803        // create a time zone by using "GMT" followed by the offset.
804        return TimeZone.getTimeZone("GMT" + offSetStr);
805    }
806
807    /** Lazily constructed internal representations. */
808    private volatile Calendar calendar;
809    private volatile Date date;
810    private volatile String stringValue;
811    private volatile long timeMS;
812
813    private GeneralizedTime(final Calendar calendar, final Date date, final long time,
814            final String stringValue) {
815        this.calendar = calendar;
816        this.date = date;
817        this.timeMS = time;
818        this.stringValue = stringValue;
819    }
820
821    /** {@inheritDoc} */
822    @Override
823    public int compareTo(final GeneralizedTime o) {
824        final Long timeMS1 = getTimeInMillis();
825        final Long timeMS2 = o.getTimeInMillis();
826        return timeMS1.compareTo(timeMS2);
827    }
828
829    /** {@inheritDoc} */
830    @Override
831    public boolean equals(final Object obj) {
832        if (this == obj) {
833            return true;
834        } else if (obj instanceof GeneralizedTime) {
835            return getTimeInMillis() == ((GeneralizedTime) obj).getTimeInMillis();
836        } else {
837            return false;
838        }
839    }
840
841    /**
842     * Returns the value of this generalized time in milliseconds since the
843     * epoch.
844     *
845     * @return The value of this generalized time in milliseconds since the
846     *         epoch.
847     */
848    public long getTimeInMillis() {
849        long tmpTimeMS = timeMS;
850        if (tmpTimeMS == Long.MIN_VALUE) {
851            if (date != null) {
852                tmpTimeMS = date.getTime();
853            } else {
854                tmpTimeMS = calendar.getTimeInMillis();
855            }
856            timeMS = tmpTimeMS;
857        }
858        return tmpTimeMS;
859    }
860
861    /** {@inheritDoc} */
862    @Override
863    public int hashCode() {
864        return ((Long) getTimeInMillis()).hashCode();
865    }
866
867    /**
868     * Returns a {@code Calendar} representation of this generalized time.
869     * <p>
870     * Subsequent modifications to the returned calendar will not alter the
871     * internal state of this generalized time.
872     *
873     * @return A {@code Calendar} representation of this generalized time.
874     */
875    public Calendar toCalendar() {
876        return (Calendar) getCalendar().clone();
877    }
878
879    /**
880     * Returns a {@code Date} representation of this generalized time.
881     * <p>
882     * Subsequent modifications to the returned date will not alter the internal
883     * state of this generalized time.
884     *
885     * @return A {@code Date} representation of this generalized time.
886     */
887    public Date toDate() {
888        Date tmpDate = date;
889        if (tmpDate == null) {
890            tmpDate = new Date(getTimeInMillis());
891            date = tmpDate;
892        }
893        return (Date) tmpDate.clone();
894    }
895
896    /** {@inheritDoc} */
897    @Override
898    public String toString() {
899        String tmpString = stringValue;
900        if (tmpString == null) {
901            // Do this in a thread-safe non-synchronized fashion.
902            // (Simple)DateFormat is neither fast nor thread-safe.
903            final StringBuilder sb = new StringBuilder(19);
904            final Calendar tmpCalendar = getCalendar();
905
906            // Format the year yyyy.
907            int n = tmpCalendar.get(Calendar.YEAR);
908            if (n < 0) {
909                throw new IllegalArgumentException("Year cannot be < 0:" + n);
910            } else if (n < 10) {
911                sb.append("000");
912            } else if (n < 100) {
913                sb.append("00");
914            } else if (n < 1000) {
915                sb.append("0");
916            }
917            sb.append(n);
918
919            // Format the month MM.
920            n = tmpCalendar.get(Calendar.MONTH) + 1;
921            if (n < 10) {
922                sb.append("0");
923            }
924            sb.append(n);
925
926            // Format the day dd.
927            n = tmpCalendar.get(Calendar.DAY_OF_MONTH);
928            if (n < 10) {
929                sb.append("0");
930            }
931            sb.append(n);
932
933            // Format the hour HH.
934            n = tmpCalendar.get(Calendar.HOUR_OF_DAY);
935            if (n < 10) {
936                sb.append("0");
937            }
938            sb.append(n);
939
940            // Format the minute mm.
941            n = tmpCalendar.get(Calendar.MINUTE);
942            if (n < 10) {
943                sb.append("0");
944            }
945            sb.append(n);
946
947            // Format the seconds ss.
948            n = tmpCalendar.get(Calendar.SECOND);
949            if (n < 10) {
950                sb.append("0");
951            }
952            sb.append(n);
953
954            // Format the milli-seconds.
955            n = tmpCalendar.get(Calendar.MILLISECOND);
956            if (n != 0) {
957                sb.append('.');
958                if (n < 10) {
959                    sb.append("00");
960                } else if (n < 100) {
961                    sb.append("0");
962                }
963                sb.append(n);
964            }
965
966            // Format the timezone.
967            n = tmpCalendar.get(Calendar.ZONE_OFFSET) + tmpCalendar.get(Calendar.DST_OFFSET);
968            if (n == 0) {
969                sb.append('Z');
970            } else {
971                if (n < 0) {
972                    sb.append('-');
973                    n = -n;
974                } else {
975                    sb.append('+');
976                }
977                n = n / 60000; // Minutes.
978
979                final int h = n / 60;
980                if (h < 10) {
981                    sb.append("0");
982                }
983                sb.append(h);
984
985                final int m = n % 60;
986                if (m < 10) {
987                    sb.append("0");
988                }
989                sb.append(m);
990            }
991            tmpString = sb.toString();
992            stringValue = tmpString;
993        }
994        return tmpString;
995    }
996
997    private Calendar getCalendar() {
998        Calendar tmpCalendar = calendar;
999        if (tmpCalendar == null) {
1000            tmpCalendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
1001            tmpCalendar.setLenient(false);
1002            tmpCalendar.setTimeInMillis(getTimeInMillis());
1003            calendar = tmpCalendar;
1004        }
1005        return tmpCalendar;
1006    }
1007}