From e399742925d1a8a1ae3dae4a86bf25d3d02e8f9c Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 09 May 2012 15:28:30 +0000
Subject: [PATCH] Fix OPENDJ-355: Add fluent API for decoding attributes

---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java |  594 ++++++++++++++++++++++++++++++++--------------------------
 1 files changed, 326 insertions(+), 268 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java
similarity index 71%
rename from opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java
rename to opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java
index 953e92a..4dd8b96 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java
@@ -23,7 +23,7 @@
  *
  *      Copyright 2012 ForgeRock AS.
  */
-package com.forgerock.opendj.util;
+package org.forgerock.opendj.ldap;
 
 import static org.forgerock.opendj.ldap.CoreMessages.*;
 
@@ -33,30 +33,88 @@
 import java.util.TimeZone;
 
 import org.forgerock.i18n.LocalizableMessage;
-import org.forgerock.opendj.ldap.ByteSequence;
-import org.forgerock.opendj.ldap.ByteString;
-import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+
+import com.forgerock.opendj.util.Validator;
 
 /**
- * Utility class for encoding and decoding generalized time syntax values.
+ * An LDAP generalized time as defined in RFC 4517. This class facilitates
+ * parsing of generalized time values to and from {@link Date} and
+ * {@link Calendar} classes.
+ * <p>
+ * The following are examples of generalized time values:
+ *
+ * <pre>
+ * 199412161032Z
+ * 199412160532-0500
+ * </pre>
+ *
+ * @see <a href="http://tools.ietf.org/html/rfc4517#section-3.3.13">RFC 4517 -
+ *      Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching
+ *      Rules </a>
  */
-public final class GeneralizedTime {
+public final class GeneralizedTime implements Comparable<GeneralizedTime> {
 
     // UTC TimeZone is assumed to never change over JVM lifetime
     private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone("UTC");
 
     /**
-     * Returns the provided generalized time syntax value decoded as a
-     * {@code Calendar}.
+     * Returns a generalized time representing the provided {@code Calendar}.
+     * <p>
+     * The provided calendar will be defensively copied in order to preserve
+     * immutability.
      *
-     * @param value
-     *            The generalized time value to be decoded.
-     * @return The decoded {@code Calendar}.
-     * @throws DecodeException
-     *             If the provided value cannot be parsed as a valid generalized
-     *             time string.
+     * @param calendar
+     *            The calendar to be converted to a generalized time.
+     * @return A generalized time representing the provided {@code Calendar}.
      */
-    public static Calendar decode(final ByteSequence value) throws DecodeException {
+    public static GeneralizedTime valueOf(final Calendar calendar) {
+        Validator.ensureNotNull(calendar);
+        return new GeneralizedTime((Calendar) calendar.clone(), null, -1L, null);
+    }
+
+    /**
+     * Returns a generalized time representing the provided {@code Date}.
+     * <p>
+     * The provided date will be defensively copied in order to preserve
+     * immutability.
+     *
+     * @param date
+     *            The date to be converted to a generalized time.
+     * @return A generalized time representing the provided {@code Date}.
+     */
+    public static GeneralizedTime valueOf(final Date date) {
+        Validator.ensureNotNull(date);
+        return new GeneralizedTime(null, (Date) date.clone(), -1L, null);
+    }
+
+    /**
+     * Returns a generalized time representing the provided time in milliseconds
+     * since the epoch.
+     *
+     * @param timeMS
+     *            The time to be converted to a generalized time.
+     * @return A generalized time representing the provided time in milliseconds
+     *         since the epoch.
+     */
+    public static GeneralizedTime valueOf(final long timeMS) {
+        Validator.ensureTrue(timeMS >= 0, "timeMS must be >= 0");
+        return new GeneralizedTime(null, null, timeMS, null);
+    }
+
+    /**
+     * Parses the provided string as an LDAP generalized time.
+     *
+     * @param time
+     *            The generalized time value to be parsed.
+     * @return The parsed generalized time.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code time} cannot be parsed as a valid generalized time
+     *             string.
+     * @throws NullPointerException
+     *             If {@code time} was {@code null}.
+     */
+    public static GeneralizedTime valueOf(final String time) {
         int year = 0;
         int month = 0;
         int day = 0;
@@ -66,14 +124,12 @@
 
         // Get the value as a string and verify that it is at least long
         // enough for "YYYYMMDDhhZ", which is the shortest allowed value.
-        final String valueString = value.toString().toUpperCase();
+        final String valueString = time.toUpperCase();
         final int length = valueString.length();
         if (length < 11) {
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // The first four characters are the century and year, and they must
@@ -124,9 +180,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR.get(valueString, String
                                 .valueOf(valueString.charAt(i)));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
         }
 
@@ -178,9 +232,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                 valueString.substring(4, 6));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
         case '1':
@@ -202,18 +254,14 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                 valueString.substring(4, 6));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
         default:
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString, valueString
                             .substring(4, 6));
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // The next two characters should be the day of the month, and they
@@ -267,9 +315,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, valueString
                                 .substring(6, 8));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -320,9 +366,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, valueString
                                 .substring(6, 8));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -373,9 +417,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, valueString
                                 .substring(6, 8));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -394,9 +436,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, valueString
                                 .substring(6, 8));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -404,9 +444,7 @@
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString, valueString
                             .substring(6, 8));
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // The next two characters must be the hour, and they must form the
@@ -460,9 +498,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, valueString
                                 .substring(8, 10));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -512,9 +548,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, valueString
                                 .substring(8, 10));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -540,9 +574,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, valueString
                                 .substring(8, 10));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -550,9 +582,7 @@
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString, valueString
                             .substring(8, 10));
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // Next, there should be either two digits comprising an integer
@@ -574,9 +604,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(m1), 10);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             minute = 10 * (m1 - '0');
@@ -625,9 +653,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE.get(valueString,
                                 valueString.substring(10, 12));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             break;
@@ -641,9 +667,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(m1), 10);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         case '+':
@@ -657,9 +681,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(m1), 10);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         case '.':
@@ -671,9 +693,7 @@
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                             .valueOf(m1), 10);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // Next, there should be either two digits comprising an integer
@@ -695,9 +715,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(s1), 12);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             second = 10 * (s1 - '0');
@@ -746,9 +764,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE.get(valueString,
                                 valueString.substring(12, 14));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             break;
@@ -760,18 +776,14 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(s1), 12);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             if (valueString.charAt(13) != '0') {
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get(valueString,
                                 valueString.substring(12, 14));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
             second = 60;
@@ -786,9 +798,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(s1), 12);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         case '+':
@@ -802,9 +812,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(s1), 12);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         case '.':
@@ -816,9 +824,7 @@
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                             .valueOf(s1), 12);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // Next, there should be either a period or comma followed by
@@ -840,9 +846,7 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(valueString.charAt(14)), 14);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         case '+':
@@ -856,129 +860,19 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                                 .valueOf(valueString.charAt(14)), 14);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
 
         default:
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, String
                             .valueOf(valueString.charAt(14)), 14);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
     }
 
     /**
-     * Returns the generalized time syntax encoding of the provided
-     * {@code Calendar}.
-     *
-     * @param value
-     *            The calendar to be encoded.
-     * @return The generalized time syntax encoding.
-     */
-    public static ByteString encode(final Calendar value) {
-        return encode(value.getTimeInMillis());
-    }
-
-    /**
-     * Returns the generalized time syntax encoding of the provided {@code Date}
-     * .
-     *
-     * @param value
-     *            The date to be encoded.
-     * @return The generalized time syntax encoding.
-     */
-    public static ByteString encode(final Date value) {
-        return encode(value.getTime());
-    }
-
-    /**
-     * Returns the generalized time syntax encoding of the provided date
-     * represented as milliseconds since the epoch.
-     *
-     * @param value
-     *            The date in milli-seconds since the epoch.
-     * @return The generalized time syntax encoding.
-     */
-    public static ByteString encode(final long value) {
-        // Generalized time has the format yyyyMMddHHmmss.SSS'Z'
-
-        // Do this in a thread-safe non-synchronized fashion.
-        // (Simple)DateFormat is neither fast nor thread-safe.
-        final StringBuilder sb = new StringBuilder(19);
-        final GregorianCalendar calendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
-        calendar.setLenient(false);
-        calendar.setTimeInMillis(value);
-
-        // Format the year yyyy.
-        int n = calendar.get(Calendar.YEAR);
-        if (n < 0) {
-            throw new IllegalArgumentException("Year cannot be < 0:" + n);
-        } else if (n < 10) {
-            sb.append("000");
-        } else if (n < 100) {
-            sb.append("00");
-        } else if (n < 1000) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the month MM.
-        n = calendar.get(Calendar.MONTH) + 1;
-        if (n < 10) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the day dd.
-        n = calendar.get(Calendar.DAY_OF_MONTH);
-        if (n < 10) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the hour HH.
-        n = calendar.get(Calendar.HOUR_OF_DAY);
-        if (n < 10) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the minute mm.
-        n = calendar.get(Calendar.MINUTE);
-        if (n < 10) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the seconds ss.
-        n = calendar.get(Calendar.SECOND);
-        if (n < 10) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the milli-seconds.
-        sb.append('.');
-        n = calendar.get(Calendar.MILLISECOND);
-        if (n < 10) {
-            sb.append("00");
-        } else if (n < 100) {
-            sb.append("0");
-        }
-        sb.append(n);
-
-        // Format the timezone (always Z).
-        sb.append('Z');
-
-        return ByteString.valueOf(sb.toString());
-    }
-
-    /**
-     * Returns a Calendar object representing the provided date / time
+     * Returns a generalized time object representing the provided date / time
      * parameters.
      *
      * @param value
@@ -997,29 +891,26 @@
      *            The second.
      * @param tz
      *            The timezone.
-     * @return A Calendar object representing the provided date / time
+     * @return A generalized time representing the provided date / time
      *         parameters.
-     * @throws DecodeException
-     *             If the calendar could not be created.
+     * @throws LocalizedIllegalArgumentException
+     *             If the generalized time could not be created.
      */
-    private static Calendar createTime(final String value, final int year, final int month,
-            final int day, final int hour, final int minute, final int second, final TimeZone tz)
-            throws DecodeException {
+    private static GeneralizedTime createTime(final String value, final int year, final int month,
+            final int day, final int hour, final int minute, final int second, final TimeZone tz) {
         try {
             final GregorianCalendar calendar = new GregorianCalendar();
             calendar.setLenient(false);
             calendar.setTimeZone(tz);
             calendar.set(year, month, day, hour, minute, second);
             calendar.set(Calendar.MILLISECOND, 0);
-            return calendar;
+            return new GeneralizedTime(calendar, null, -1L, value);
         } catch (final Exception e) {
             // This should only happen if the provided date wasn't legal
             // (e.g., September 31).
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
-            final DecodeException de = DecodeException.error(message, e);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", de);
-            throw de;
+            throw new LocalizedIllegalArgumentException(message, e);
         }
     }
 
@@ -1053,13 +944,13 @@
      *            should be 1000.
      * @return The timestamp created from the provided generalized time value
      *         including the fractional element.
-     * @throws DecodeException
+     * @throws LocalizedIllegalArgumentException
      *             If the provided value cannot be parsed as a valid generalized
      *             time string.
      */
-    private static Calendar finishDecodingFraction(final String value, final int startPos,
+    private static GeneralizedTime finishDecodingFraction(final String value, final int startPos,
             final int year, final int month, final int day, final int hour, final int minute,
-            final int second, final int multiplier) throws DecodeException {
+            final int second, final int multiplier) {
         final int length = value.length();
         final StringBuilder fractionBuffer = new StringBuilder((2 + length) - startPos);
         fractionBuffer.append("0.");
@@ -1089,10 +980,7 @@
                     final LocalizableMessage message =
                             WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value,
                                     String.valueOf(c));
-                    final DecodeException e = DecodeException.error(message);
-                    StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax",
-                            "finishDecodingFraction", e);
-                    throw e;
+                    throw new LocalizedIllegalArgumentException(message);
                 }
 
                 timeZone = TIME_ZONE_UTC_OBJ;
@@ -1107,27 +995,20 @@
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.get(value, String
                                 .valueOf(c));
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG
-                        .throwing("GeneralizedTimeSyntax", "finishDecodingFraction", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
         }
 
         if (fractionBuffer.length() == 2) {
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "finishDecodingFraction", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         if (timeZone == null) {
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "finishDecodingFraction", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         final Double fractionValue = Double.parseDouble(fractionBuffer.toString());
@@ -1139,16 +1020,13 @@
             calendar.setTimeZone(timeZone);
             calendar.set(year, month, day, hour, minute, second);
             calendar.set(Calendar.MILLISECOND, additionalMilliseconds);
-            return calendar;
+            return new GeneralizedTime(calendar, null, -1L, value);
         } catch (final Exception e) {
-
             // This should only happen if the provided date wasn't legal
             // (e.g., September 31).
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, String.valueOf(e));
-            final DecodeException de = DecodeException.error(message, e);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "valueIsAcceptable", de);
-            throw de;
+            throw new LocalizedIllegalArgumentException(message, e);
         }
     }
 
@@ -1165,15 +1043,12 @@
      * @throws DecodeException
      *             If the provided value does not contain a valid offset.
      */
-    private static TimeZone getTimeZoneForOffset(final String value, final int startPos)
-            throws DecodeException {
+    private static TimeZone getTimeZoneForOffset(final String value, final int startPos) {
         final String offSetStr = value.substring(startPos);
         if ((offSetStr.length() != 3) && (offSetStr.length() != 5)) {
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // The first character must be either a plus or minus.
@@ -1186,9 +1061,7 @@
         default:
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // The first two characters must be an integer between 00 and 23.
@@ -1212,9 +1085,7 @@
             default:
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
@@ -1230,18 +1101,14 @@
             default:
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
             break;
 
         default:
             final LocalizableMessage message =
                     WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-            final DecodeException e = DecodeException.error(message);
-            StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-            throw e;
+            throw new LocalizedIllegalArgumentException(message);
         }
 
         // If there are two more characters, then they must be an integer
@@ -1271,19 +1138,14 @@
                 default:
                     final LocalizableMessage message =
                             WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-                    final DecodeException e = DecodeException.error(message);
-                    StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset",
-                            e);
-                    throw e;
+                    throw new LocalizedIllegalArgumentException(message);
                 }
                 break;
 
             default:
                 final LocalizableMessage message =
                         WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(value, offSetStr);
-                final DecodeException e = DecodeException.error(message);
-                StaticUtils.DEBUG_LOG.throwing("GeneralizedTimeSyntax", "getTimeZoneForOffset", e);
-                throw e;
+                throw new LocalizedIllegalArgumentException(message);
             }
         }
 
@@ -1292,8 +1154,204 @@
         return TimeZone.getTimeZone("GMT" + offSetStr);
     }
 
-    private GeneralizedTime() {
-        // Prevent instantiation.
+    // Lazily constructed internal representations.
+    private volatile Calendar calendar;
+    private volatile Date date;
+    private volatile String stringValue;
+    private volatile long timeMS;
+
+    private GeneralizedTime(final Calendar calendar, final Date date, final long time,
+            final String stringValue) {
+        this.calendar = calendar;
+        this.date = date;
+        this.timeMS = time;
+        this.stringValue = stringValue;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public int compareTo(final GeneralizedTime o) {
+        final Long timeMS1 = getTimeInMillis();
+        final Long timeMS2 = o.getTimeInMillis();
+        return timeMS1.compareTo(timeMS2);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj instanceof GeneralizedTime) {
+            return getTimeInMillis() == ((GeneralizedTime) obj).getTimeInMillis();
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the value of this generalized time in milliseconds since the
+     * epoch.
+     *
+     * @return The value of this generalized time in milliseconds since the
+     *         epoch.
+     */
+    public long getTimeInMillis() {
+        long tmpTimeMS = timeMS;
+        if (tmpTimeMS == -1) {
+            if (date != null) {
+                tmpTimeMS = date.getTime();
+            } else {
+                tmpTimeMS = calendar.getTimeInMillis();
+            }
+            timeMS = tmpTimeMS;
+        }
+        return tmpTimeMS;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int hashCode() {
+        return ((Long) getTimeInMillis()).hashCode();
+    }
+
+    /**
+     * Returns a {@code Calendar} representation of this generalized time.
+     * <p>
+     * Subsequent modifications to the returned calendar will not alter the
+     * internal state of this generalized time.
+     *
+     * @return A {@code Calendar} representation of this generalized time.
+     */
+    public Calendar toCalendar() {
+        Calendar tmpCalendar = calendar;
+        if (tmpCalendar == null) {
+            tmpCalendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
+            tmpCalendar.setLenient(false);
+            tmpCalendar.setTimeInMillis(getTimeInMillis());
+            calendar = tmpCalendar;
+        }
+        return (Calendar) tmpCalendar.clone();
+    }
+
+    /**
+     * Returns a {@code Date} representation of this generalized time.
+     * <p>
+     * Subsequent modifications to the returned date will not alter the internal
+     * state of this generalized time.
+     *
+     * @return A {@code Date} representation of this generalized time.
+     */
+    public Date toDate() {
+        Date tmpDate = date;
+        if (tmpDate == null) {
+            tmpDate = new Date(getTimeInMillis());
+            date = tmpDate;
+        }
+        return (Date) tmpDate.clone();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() {
+        String tmpString = stringValue;
+        if (tmpString == null) {
+            // Do this in a thread-safe non-synchronized fashion.
+            // (Simple)DateFormat is neither fast nor thread-safe.
+            final StringBuilder sb = new StringBuilder(19);
+            final Calendar tmpCalendar = toCalendar();
+
+            // Format the year yyyy.
+            int n = tmpCalendar.get(Calendar.YEAR);
+            if (n < 0) {
+                throw new IllegalArgumentException("Year cannot be < 0:" + n);
+            } else if (n < 10) {
+                sb.append("000");
+            } else if (n < 100) {
+                sb.append("00");
+            } else if (n < 1000) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the month MM.
+            n = tmpCalendar.get(Calendar.MONTH) + 1;
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the day dd.
+            n = tmpCalendar.get(Calendar.DAY_OF_MONTH);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the hour HH.
+            n = tmpCalendar.get(Calendar.HOUR_OF_DAY);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the minute mm.
+            n = tmpCalendar.get(Calendar.MINUTE);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the seconds ss.
+            n = tmpCalendar.get(Calendar.SECOND);
+            if (n < 10) {
+                sb.append("0");
+            }
+            sb.append(n);
+
+            // Format the milli-seconds.
+            n = tmpCalendar.get(Calendar.MILLISECOND);
+            if (n != 0) {
+                sb.append('.');
+                if (n < 10) {
+                    sb.append("00");
+                } else if (n < 100) {
+                    sb.append("0");
+                }
+                sb.append(n);
+            }
+
+            // Format the timezone.
+            n = tmpCalendar.get(Calendar.ZONE_OFFSET); /* ms */
+            if (n == 0) {
+                sb.append('Z');
+            } else {
+                if (n < 0) {
+                    sb.append('-');
+                    n = -n;
+                } else {
+                    sb.append('+');
+                }
+                n = n / 60000; // Minutes.
+
+                final int h = n / 60;
+                if (h < 10) {
+                    sb.append("0");
+                }
+                sb.append(h);
+
+                final int m = n % 60;
+                if (m < 10) {
+                    sb.append("0");
+                }
+                sb.append(m);
+            }
+            tmpString = sb.toString();
+            stringValue = tmpString;
+        }
+        return stringValue;
+    }
 }

--
Gitblit v1.10.0