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