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