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