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/com/forgerock/opendj/util/Functions.java                                      |  435 +++++++++----
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java |   10 
 opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties                                |    4 
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java                            |  132 ++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java                                |  194 +++--
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java                        |  360 ++++++++++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/GeneralizedTime.java                                |  594 ++++++++++--------
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java |   10 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java               |   11 
 9 files changed, 1,251 insertions(+), 499 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/Functions.java b/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/Functions.java
index 64c21f8..87302c0 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/Functions.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/Functions.java
@@ -27,13 +27,16 @@
 
 package com.forgerock.opendj.util;
 
-import java.util.Calendar;
+import static org.forgerock.opendj.ldap.CoreMessages.FUNCTIONS_TO_INTEGER_FAIL;
+import static org.forgerock.opendj.ldap.CoreMessages.FUNCTIONS_TO_LONG_FAIL;
+import static org.forgerock.opendj.ldap.CoreMessages.WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN;
 
+import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.GeneralizedTime;
 import org.forgerock.opendj.ldap.schema.Schema;
 
 /**
@@ -60,28 +63,45 @@
 
     }
 
-    private static final Function<ByteString, AttributeDescription, Schema> BYTESTRING_TO_ATTRIBUTE_DESCRIPTION =
-            new Function<ByteString, AttributeDescription, Schema>() {
-
-                public AttributeDescription apply(final ByteString value, final Schema p) {
-                    // FIXME: what should we do if parsing fails?
-                    return AttributeDescription.valueOf(value.toString(), p);
-                }
-            };
-
-    private static final Function<ByteString, String, Void> BYTESTRING_TO_BASE64 =
+    private static final Function<ByteString, String, Void> BYTESTRING_TO_STRING =
             new Function<ByteString, String, Void>() {
-
                 public String apply(final ByteString value, final Void p) {
-                    return Base64.encode(value);
+                    return value.toString();
                 }
             };
 
-    private static final Function<ByteString, Boolean, Void> BYTESTRING_TO_BOOLEAN =
-            new Function<ByteString, Boolean, Void>() {
+    private static final Function<Object, Object, Void> IDENTITY =
+            new Function<Object, Object, Void>() {
+                public Object apply(final Object value, final Void p) {
+                    return value;
+                }
+            };
 
-                public Boolean apply(final ByteString value, final Void p) {
-                    final String valueString = StaticUtils.toLowerCase(value.toString());
+    private static final Function<String, String, Void> NORMALIZE_STRING =
+            new Function<String, String, Void>() {
+                public String apply(final String value, final Void p) {
+                    return StaticUtils.toLowerCase(value).trim();
+                }
+            };
+
+    private static final Function<Object, ByteString, Void> OBJECT_TO_BYTESTRING =
+            new Function<Object, ByteString, Void>() {
+                public ByteString apply(final Object value, final Void p) {
+                    return ByteString.valueOf(value);
+                }
+            };
+
+    private static final Function<String, AttributeDescription, Schema> STRING_TO_ATTRIBUTE_DESCRIPTION =
+            new Function<String, AttributeDescription, Schema>() {
+                public AttributeDescription apply(final String value, final Schema p) {
+                    return AttributeDescription.valueOf(value, p);
+                }
+            };
+
+    private static final Function<String, Boolean, Void> STRING_TO_BOOLEAN =
+            new Function<String, Boolean, Void>() {
+                public Boolean apply(final String value, final Void p) {
+                    final String valueString = StaticUtils.toLowerCase(value);
 
                     if (valueString.equals("true") || valueString.equals("yes")
                             || valueString.equals("on") || valueString.equals("1")) {
@@ -90,86 +110,159 @@
                             || valueString.equals("off") || valueString.equals("0")) {
                         return Boolean.FALSE;
                     } else {
-                        throw new NumberFormatException("Invalid boolean value \"" + valueString
-                                + "\"");
+                        final LocalizableMessage message =
+                                WARN_ATTR_SYNTAX_ILLEGAL_BOOLEAN.get(valueString);
+                        throw new LocalizedIllegalArgumentException(message);
                     }
                 }
             };
 
-    private static final Function<ByteString, Calendar, Void> BYTESTRING_TO_CALENDAR =
-            new Function<ByteString, Calendar, Void>() {
+    private static final Function<String, DN, Schema> STRING_TO_DN =
+            new Function<String, DN, Schema>() {
+                public DN apply(final String value, final Schema p) {
+                    return DN.valueOf(value, p);
+                }
+            };
 
-                public Calendar apply(final ByteString value, final Void p) {
+    private static final Function<String, GeneralizedTime, Void> STRING_TO_GENERALIZED_TIME =
+            new Function<String, GeneralizedTime, Void>() {
+                public GeneralizedTime apply(final String value, final Void p) {
+                    return GeneralizedTime.valueOf(value);
+                }
+            };
+
+    private static final Function<String, Integer, Void> STRING_TO_INTEGER =
+            new Function<String, Integer, Void>() {
+                public Integer apply(final String value, final Void p) {
                     try {
-                        return GeneralizedTime.decode(value);
-                    } catch (DecodeException e) {
-                        throw new LocalizedIllegalArgumentException(e.getMessageObject(), e);
+                        return Integer.valueOf(value);
+                    } catch (final NumberFormatException e) {
+                        final LocalizableMessage message = FUNCTIONS_TO_INTEGER_FAIL.get(value);
+                        throw new LocalizedIllegalArgumentException(message);
                     }
                 }
             };
 
-    private static final Function<ByteString, DN, Schema> BYTESTRING_TO_DN =
-            new Function<ByteString, DN, Schema>() {
-
-                public DN apply(final ByteString value, final Schema p) {
-                    // FIXME: what should we do if parsing fails?
-
-                    // FIXME: we should have a ByteString valueOf
-                    // implementation.
-                    return DN.valueOf(value.toString(), p);
+    private static final Function<String, Long, Void> STRING_TO_LONG =
+            new Function<String, Long, Void>() {
+                public Long apply(final String value, final Void p) {
+                    try {
+                        return Long.valueOf(value);
+                    } catch (final NumberFormatException e) {
+                        final LocalizableMessage message = FUNCTIONS_TO_LONG_FAIL.get(value);
+                        throw new LocalizedIllegalArgumentException(message);
+                    }
                 }
             };
 
-    private static final Function<ByteString, Integer, Void> BYTESTRING_TO_INTEGER =
-            new Function<ByteString, Integer, Void>() {
+    private static final Function<ByteString, AttributeDescription, Schema> BYTESTRING_TO_ATTRIBUTE_DESCRIPTION =
+            composeSecondP(valueToString(), STRING_TO_ATTRIBUTE_DESCRIPTION);
 
-                public Integer apply(final ByteString value, final Void p) {
-                    // We do not use ByteString.toInt() as we are string based.
-                    return Integer.valueOf(value.toString());
-                }
+    private static final Function<ByteString, Boolean, Void> BYTESTRING_TO_BOOLEAN = compose(
+            valueToString(), STRING_TO_BOOLEAN);
+
+    private static final Function<ByteString, DN, Schema> BYTESTRING_TO_DN = composeSecondP(
+            valueToString(), STRING_TO_DN);
+
+    private static final Function<ByteString, GeneralizedTime, Void> BYTESTRING_TO_GENERALIZED_TIME =
+            compose(valueToString(), STRING_TO_GENERALIZED_TIME);
+
+    private static final Function<ByteString, Integer, Void> BYTESTRING_TO_INTEGER = compose(
+            valueToString(), STRING_TO_INTEGER);
+
+    private static final Function<ByteString, Long, Void> BYTESTRING_TO_LONG = compose(
+            valueToString(), STRING_TO_LONG);
+
+    /**
+     * Returns the composition of two functions. The result of the first
+     * function will be passed to the second.
+     *
+     * @param <M>
+     *            The type of input values transformed by this function.
+     * @param <N>
+     *            The type of output values returned by this function.
+     * @param <X>
+     *            The type of intermediate values passed between the two
+     *            functions.
+     * @param first
+     *            The first function which will consume the input.
+     * @param second
+     *            The second function which will produce the result.
+     * @return The composition.
+     */
+    public static <M, X, N> Function<M, N, Void> compose(final Function<M, X, Void> first,
+            final Function<X, N, Void> second) {
+        return new Function<M, N, Void>() {
+            public N apply(final M value, final Void p) {
+                final X tmp = first.apply(value, p);
+                return second.apply(tmp, p);
             };
+        };
+    }
 
-    private static final Function<ByteString, Long, Void> BYTESTRING_TO_LONG =
-            new Function<ByteString, Long, Void>() {
-
-                public Long apply(final ByteString value, final Void p) {
-                    // We do not use ByteString.toLong() as we are string based.
-                    return Long.valueOf(value.toString());
-                }
+    /**
+     * Returns the composition of two functions. The result of the first
+     * function will be passed to the second. The first function will be passed
+     * an additional parameter.
+     *
+     * @param <M>
+     *            The type of input values transformed by this function.
+     * @param <N>
+     *            The type of output values returned by this function.
+     * @param <X>
+     *            The type of intermediate values passed between the two
+     *            functions.
+     * @param <P>
+     *            The type of the additional parameter to the first function's
+     *            {@code apply} method. Use {@link java.lang.Void} for functions
+     *            that do not need an additional parameter.
+     * @param first
+     *            The first function which will consume the input.
+     * @param second
+     *            The second function which will produce the result.
+     * @return The composition.
+     */
+    public static <M, X, N, P> Function<M, N, P> composeFirstP(final Function<M, X, P> first,
+            final Function<X, N, Void> second) {
+        return new Function<M, N, P>() {
+            public N apply(final M value, final P p) {
+                final X tmp = first.apply(value, p);
+                return second.apply(tmp, null);
             };
+        };
+    }
 
-    private static final Function<ByteString, String, Void> BYTESTRING_TO_STRING =
-            new Function<ByteString, String, Void>() {
-
-                public String apply(final ByteString value, final Void p) {
-                    return value.toString();
-                }
+    /**
+     * Returns the composition of two functions. The result of the first
+     * function will be passed to the second. The second function will be passed
+     * an additional parameter.
+     *
+     * @param <M>
+     *            The type of input values transformed by this function.
+     * @param <N>
+     *            The type of output values returned by this function.
+     * @param <X>
+     *            The type of intermediate values passed between the two
+     *            functions.
+     * @param <P>
+     *            The type of the additional parameter to the second function's
+     *            {@code apply} method. Use {@link java.lang.Void} for functions
+     *            that do not need an additional parameter.
+     * @param first
+     *            The first function which will consume the input.
+     * @param second
+     *            The second function which will produce the result.
+     * @return The composition.
+     */
+    public static <M, X, N, P> Function<M, N, P> composeSecondP(final Function<M, X, Void> first,
+            final Function<X, N, P> second) {
+        return new Function<M, N, P>() {
+            public N apply(final M value, final P p) {
+                final X tmp = first.apply(value, null);
+                return second.apply(tmp, p);
             };
-
-    private static final Function<Object, ByteString, Void> OBJECT_TO_BYTESTRING =
-            new Function<Object, ByteString, Void>() {
-
-                public ByteString apply(final Object value, final Void p) {
-                    return ByteString.valueOf(value);
-                }
-            };
-
-    private static final Function<String, String, Void> NORMALIZE_STRING =
-            new Function<String, String, Void>() {
-
-                public String apply(final String value, final Void p) {
-                    return StaticUtils.toLowerCase(value).trim();
-                }
-            };
-
-    private static final Function<Object, Object, Void> IDENTITY =
-            new Function<Object, Object, Void>() {
-
-                public Object apply(Object value, Void p) {
-                    return value;
-                }
-
-            };
+        };
+    }
 
     /**
      * Returns a function which which always invokes {@code function} with
@@ -232,28 +325,115 @@
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as an {@code AttributeDescription} using the default
-     * schema. Invalid values will result in a
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * default schema. Invalid values will result in a
      * {@code LocalizedIllegalArgumentException}.
      *
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as an {@code AttributeDescription}.
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<String, AttributeDescription, Void> stringToAttributeDescription() {
+        return fixedFunction(STRING_TO_ATTRIBUTE_DESCRIPTION, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * provided schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @param schema
+     *            The schema to use for decoding attribute descriptions.
+     * @return A function which parses {@code AttributeDescription}s.
+     */
+    public static Function<String, AttributeDescription, Void> stringToAttributeDescription(
+            final Schema schema) {
+        return fixedFunction(STRING_TO_ATTRIBUTE_DESCRIPTION, schema);
+    }
+
+    /**
+     * Returns a function which parses {@code Boolean} values. The function will
+     * accept the values {@code 0}, {@code false}, {@code no}, {@code off},
+     * {@code 1}, {@code true}, {@code yes}, {@code on}. All other values will
+     * result in a {@code NumberFormatException}.
+     *
+     * @return A function which parses {@code Boolean} values.
+     */
+    public static Function<String, Boolean, Void> stringToBoolean() {
+        return STRING_TO_BOOLEAN;
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the default schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<String, DN, Void> stringToDN() {
+        return fixedFunction(STRING_TO_DN, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Returns a function which parses {@code DN}s using the provided schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
+     *
+     * @param schema
+     *            The schema to use for decoding DNs.
+     * @return A function which parses {@code DN}s.
+     */
+    public static Function<String, DN, Void> stringToDN(final Schema schema) {
+        return fixedFunction(STRING_TO_DN, schema);
+    }
+
+    /**
+     * Returns a function which parses generalized time strings. Invalid values
+     * will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses generalized time strings.
+     */
+    public static Function<String, GeneralizedTime, Void> stringToGeneralizedTime() {
+        return STRING_TO_GENERALIZED_TIME;
+    }
+
+    /**
+     * Returns a function which parses {@code Integer} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Integer} string values.
+     */
+    public static Function<String, Integer, Void> stringToInteger() {
+        return STRING_TO_INTEGER;
+    }
+
+    /**
+     * Returns a function which parses {@code Long} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Long} string values.
+     */
+    public static Function<String, Long, Void> stringToLong() {
+        return STRING_TO_LONG;
+    }
+
+    /**
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * default schema. Invalid values will result in a
+     * {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code AttributeDescription}s.
      */
     public static Function<ByteString, AttributeDescription, Void> valueToAttributeDescription() {
         return fixedFunction(BYTESTRING_TO_ATTRIBUTE_DESCRIPTION, Schema.getDefaultSchema());
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as an {@code AttributeDescription} using the provided
-     * schema. Invalid values will result in a
+     * Returns a function which parses {@code AttributeDescription}s using the
+     * provided schema. Invalid values will result in a
      * {@code LocalizedIllegalArgumentException}.
      *
      * @param schema
      *            The schema to use for decoding attribute descriptions.
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as an {@code AttributeDescription}.
+     * @return A function which parses {@code AttributeDescription}s.
      */
     public static Function<ByteString, AttributeDescription, Void> valueToAttributeDescription(
             final Schema schema) {
@@ -261,85 +441,66 @@
     }
 
     /**
-     * Returns a function which encodes a {@code ByteString} as {@code Base64}.
+     * Returns a function which parses {@code Boolean} values. The function will
+     * accept the values {@code 0}, {@code false}, {@code no}, {@code off},
+     * {@code 1}, {@code true}, {@code yes}, {@code on}. All other values will
+     * result in a {@code NumberFormatException}.
      *
-     * @return A function which encodes a {@code ByteString} as {@code Base64}.
-     */
-    public static Function<ByteString, String, Void> valueToBase64() {
-        return BYTESTRING_TO_BASE64;
-    }
-
-    /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} to a {@code Boolean}. The function will accept the
-     * values {@code 0}, {@code false}, {@code no}, {@code off}, {@code 1},
-     * {@code true}, {@code yes}, {@code on}. All other values will result in a
-     * {@code NumberFormatException}.
-     *
-     * @return A function which transforms a {@code ByteString} to a
-     *         {@code Boolean}.
+     * @return A function which parses {@code Boolean} values.
      */
     public static Function<ByteString, Boolean, Void> valueToBoolean() {
         return BYTESTRING_TO_BOOLEAN;
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as a generalized time syntax. Invalid values will
-     * result in a {@code LocalizedIllegalArgumentException}.
+     * Returns a function which parses {@code DN}s using the default schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
      *
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as generalized time syntax.
-     */
-    public static Function<ByteString, Calendar, Void> valueToCalendar() {
-        return BYTESTRING_TO_CALENDAR;
-    }
-
-    /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as a {@code DN} using the default schema. Invalid
-     * values will result in a {@code LocalizedIllegalArgumentException}.
-     *
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as an {@code DN}.
+     * @return A function which parses {@code DN}s.
      */
     public static Function<ByteString, DN, Void> valueToDN() {
         return fixedFunction(BYTESTRING_TO_DN, Schema.getDefaultSchema());
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as a {@code DN} using the provided schema. Invalid
-     * values will result in a {@code LocalizedIllegalArgumentException}.
+     * Returns a function which parses {@code DN}s using the provided schema.
+     * Invalid values will result in a {@code LocalizedIllegalArgumentException}
+     * .
      *
      * @param schema
      *            The schema to use for decoding DNs.
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as an {@code DN}.
+     * @return A function which parses {@code DN}s.
      */
     public static Function<ByteString, DN, Void> valueToDN(final Schema schema) {
         return fixedFunction(BYTESTRING_TO_DN, schema);
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as an {@code Integer}. Invalid values will result in a
-     * {@code NumberFormatException}.
+     * Returns a function which parses generalized time strings. Invalid values
+     * will result in a {@code LocalizedIllegalArgumentException}.
      *
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as an {@code Integer}.
+     * @return A function which parses generalized time strings.
+     */
+    public static Function<ByteString, GeneralizedTime, Void> valueToGeneralizedTime() {
+        return BYTESTRING_TO_GENERALIZED_TIME;
+    }
+
+    /**
+     * Returns a function which parses {@code Integer} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
+     *
+     * @return A function which parses {@code Integer} string values.
      */
     public static Function<ByteString, Integer, Void> valueToInteger() {
         return BYTESTRING_TO_INTEGER;
     }
 
     /**
-     * Returns a function which parses the string representation of a
-     * {@code ByteString} as a {@code Long}. Invalid values will result in a
-     * {@code NumberFormatException}.
+     * Returns a function which parses {@code Long} string values. Invalid
+     * values will result in a {@code LocalizedIllegalArgumentException}.
      *
-     * @return A function which parses the string representation of a
-     *         {@code ByteString} as a {@code Long}.
+     * @return A function which parses {@code Long} string values.
      */
     public static Function<ByteString, Long, Void> valueToLong() {
         return BYTESTRING_TO_LONG;
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
index ecffa5c..6606072 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
@@ -26,17 +26,17 @@
 
 package org.forgerock.opendj.ldap;
 
+import static com.forgerock.opendj.util.Collections2.transformedCollection;
+
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.NoSuchElementException;
 import java.util.Set;
 
 import org.forgerock.opendj.ldap.schema.Schema;
 
-import com.forgerock.opendj.util.Base64;
-import com.forgerock.opendj.util.Collections2;
 import com.forgerock.opendj.util.Function;
 import com.forgerock.opendj.util.Functions;
 
@@ -105,7 +105,7 @@
      * @return The first value decoded as an {@code AttributeDescription}.
      */
     public AttributeDescription asAttributeDescription() {
-        return asAttributeDescription(null);
+        return asAttributeDescription((AttributeDescription) null);
     }
 
     /**
@@ -122,25 +122,16 @@
     }
 
     /**
-     * Returns the first value encoded as base64, or {@code null} if the
-     * attribute does not contain any values.
-     *
-     * @return The first value encoded as base64.
-     */
-    public String asBase64() {
-        return asBase64(null);
-    }
-
-    /**
-     * Returns the first value encoded as base64, or {@code defaultValue} if the
+     * Returns the first value decoded as an {@code AttributeDescription} using
+     * the schema associated with this parser, or {@code defaultValue} if the
      * attribute does not contain any values.
      *
      * @param defaultValue
      *            The default value to return if the attribute is empty.
-     * @return The first value encoded as base64.
+     * @return The first value decoded as an {@code AttributeDescription}.
      */
-    public String asBase64(final ByteString defaultValue) {
-        return parseSingleValue(Functions.valueToBase64(), Base64.encode(defaultValue));
+    public AttributeDescription asAttributeDescription(final String defaultValue) {
+        return asAttributeDescription(AttributeDescription.valueOf(defaultValue, getSchema()));
     }
 
     /**
@@ -188,30 +179,6 @@
     }
 
     /**
-     * Returns the first value decoded as a {@code Calendar} using the
-     * generalized time syntax, or {@code null} if the attribute does not
-     * contain any values.
-     *
-     * @return The first value decoded as a {@code Calendar}.
-     */
-    public Calendar asCalendar() {
-        return asCalendar(null);
-    }
-
-    /**
-     * Returns the first value decoded as an {@code Calendar} using the
-     * generalized time syntax, or {@code defaultValue} if the attribute does
-     * not contain any values.
-     *
-     * @param defaultValue
-     *            The default value to return if the attribute is empty.
-     * @return The first value decoded as an {@code Calendar}.
-     */
-    public Calendar asCalendar(final Calendar defaultValue) {
-        return parseSingleValue(Functions.valueToCalendar(), defaultValue);
-    }
-
-    /**
      * Returns the first value decoded as a {@code DN} using the schema
      * associated with this parser, or {@code null} if the attribute does not
      * contain any values.
@@ -219,7 +186,7 @@
      * @return The first value decoded as a {@code DN}.
      */
     public DN asDN() {
-        return asDN(null);
+        return asDN((DN) null);
     }
 
     /**
@@ -236,6 +203,43 @@
     }
 
     /**
+     * Returns the first value decoded as a {@code DN} using the schema
+     * associated with this parser, or {@code defaultValue} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as a {@code DN}.
+     */
+    public DN asDN(final String defaultValue) {
+        return asDN(DN.valueOf(defaultValue, getSchema()));
+    }
+
+    /**
+     * Returns the first value decoded as a {@code GeneralizedTime} using the
+     * generalized time syntax, or {@code null} if the attribute does not
+     * contain any values.
+     *
+     * @return The first value decoded as a {@code GeneralizedTime}.
+     */
+    public GeneralizedTime asGeneralizedTime() {
+        return asGeneralizedTime(null);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code GeneralizedTime} using the
+     * generalized time syntax, or {@code defaultValue} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValue
+     *            The default value to return if the attribute is empty.
+     * @return The first value decoded as an {@code GeneralizedTime}.
+     */
+    public GeneralizedTime asGeneralizedTime(final GeneralizedTime defaultValue) {
+        return parseSingleValue(Functions.valueToGeneralizedTime(), defaultValue);
+    }
+
+    /**
      * Returns the first value decoded as an {@code Integer}, or {@code null} if
      * the attribute does not contain any values.
      *
@@ -281,6 +285,17 @@
 
     /**
      * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or an empty set if the
+     * attribute does not contain any values.
+     *
+     * @return The values decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription() {
+        return asSetOfAttributeDescription(Collections.<AttributeDescription> emptySet());
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code AttributeDescription}s
      * using the schema associated with this parser, or {@code defaultValues} if
      * the attribute does not contain any values.
      *
@@ -308,28 +323,17 @@
     }
 
     /**
-     * Returns the values contained in the attribute encoded as base64, or
-     * {@code defaultValues} if the attribute does not contain any values.
+     * Returns the values decoded as a set of {@code AttributeDescription}s
+     * using the schema associated with this parser, or {@code defaultValues} if
+     * the attribute does not contain any values.
      *
      * @param defaultValues
      *            The default values to return if the attribute is empty.
-     * @return The values contained in the attribute encoded as base64.
+     * @return The values decoded as a set of {@code AttributeDescription}s.
      */
-    public Set<String> asSetOfBase64(final Collection<ByteString> defaultValues) {
-        return parseMultipleValues(Functions.valueToString(), Collections2.transformedCollection(
-                defaultValues, Functions.valueToBase64(), null));
-    }
-
-    /**
-     * Returns the values contained in the attribute encoded as base64, or
-     * {@code defaultValues} if the attribute does not contain any values.
-     *
-     * @param defaultValues
-     *            The default values to return if the attribute is empty.
-     * @return The values contained in the attribute encoded as base64.
-     */
-    public Set<String> asSetOfBase64(final String... defaultValues) {
-        return asSetOfString(Arrays.asList(defaultValues));
+    public Set<AttributeDescription> asSetOfAttributeDescription(final String... defaultValues) {
+        return asSetOfAttributeDescription(transformedCollection(Arrays.asList(defaultValues),
+                Functions.stringToAttributeDescription(getSchema()), null));
     }
 
     /**
@@ -381,29 +385,14 @@
     }
 
     /**
-     * Returns the values decoded as a set of {@code Calendar}s using the
-     * generalized time syntax, or {@code defaultValues} if the attribute does
-     * not contain any values.
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or an empty set if the attribute does not
+     * contain any values.
      *
-     * @param defaultValues
-     *            The default values to return if the attribute is empty.
-     * @return The values decoded as a set of {@code Calendar}s.
+     * @return The values decoded as a set of {@code DN}s.
      */
-    public Set<Calendar> asSetOfCalendar(final Calendar... defaultValues) {
-        return asSetOfCalendar(Arrays.asList(defaultValues));
-    }
-
-    /**
-     * Returns the values decoded as a set of {@code Calendar}s using the
-     * generalized time syntax, or {@code defaultValues} if the attribute does
-     * not contain any values.
-     *
-     * @param defaultValues
-     *            The default values to return if the attribute is empty.
-     * @return The values decoded as a set of {@code Calendar}s.
-     */
-    public Set<Calendar> asSetOfCalendar(final Collection<Calendar> defaultValues) {
-        return parseMultipleValues(Functions.valueToCalendar(), defaultValues);
+    public Set<DN> asSetOfDN() {
+        return asSetOfDN(Collections.<DN> emptySet());
     }
 
     /**
@@ -433,6 +422,47 @@
     }
 
     /**
+     * Returns the values decoded as a set of {@code DN}s using the schema
+     * associated with this parser, or {@code defaultValues} if the attribute
+     * does not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code DN}s.
+     */
+    public Set<DN> asSetOfDN(final String... defaultValues) {
+        return asSetOfDN(transformedCollection(Arrays.asList(defaultValues), Functions
+                .stringToDN(getSchema()), null));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code GeneralizedTime}s using the
+     * generalized time syntax, or {@code defaultValues} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code GeneralizedTime}s.
+     */
+    public Set<GeneralizedTime> asSetOfGeneralizedTime(
+            final Collection<GeneralizedTime> defaultValues) {
+        return parseMultipleValues(Functions.valueToGeneralizedTime(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code GeneralizedTime}s using the
+     * generalized time syntax, or {@code defaultValues} if the attribute does
+     * not contain any values.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code GeneralizedTime}s.
+     */
+    public Set<GeneralizedTime> asSetOfGeneralizedTime(final GeneralizedTime... defaultValues) {
+        return asSetOfGeneralizedTime(Arrays.asList(defaultValues));
+    }
+
+    /**
      * Returns the values decoded as a set of {@code Integer}s, or
      * {@code defaultValues} if the attribute does not contain any values.
      *
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;
+    }
 }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java
index e7191f6..6682e1f 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java
@@ -26,11 +26,11 @@
  */
 package org.forgerock.opendj.ldap.schema;
 
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DecodeException;
-
-import com.forgerock.opendj.util.GeneralizedTime;
+import org.forgerock.opendj.ldap.GeneralizedTime;
 
 /**
  * This class defines the generalizedTimeMatch matching rule defined in X.520
@@ -39,6 +39,10 @@
 final class GeneralizedTimeEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
     public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
             throws DecodeException {
-        return ByteString.valueOf(GeneralizedTime.decode(value).getTimeInMillis());
+        try {
+            return ByteString.valueOf(GeneralizedTime.valueOf(value.toString()).getTimeInMillis());
+        } catch (LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
     }
 }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java
index c7cb533..f28821e 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java
@@ -26,11 +26,11 @@
  */
 package org.forgerock.opendj.ldap.schema;
 
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DecodeException;
-
-import com.forgerock.opendj.util.GeneralizedTime;
+import org.forgerock.opendj.ldap.GeneralizedTime;
 
 /**
  * This class defines the generalizedTimeOrderingMatch matching rule defined in
@@ -39,6 +39,10 @@
 final class GeneralizedTimeOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
     public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
             throws DecodeException {
-        return ByteString.valueOf(GeneralizedTime.decode(value).getTimeInMillis());
+        try {
+            return ByteString.valueOf(GeneralizedTime.valueOf(value.toString()).getTimeInMillis());
+        } catch (LocalizedIllegalArgumentException e) {
+            throw DecodeException.error(e.getMessageObject());
+        }
     }
 }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java
index 1b28a5b..0ba5d70 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java
@@ -33,10 +33,9 @@
 import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GENERALIZED_TIME_NAME;
 
 import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.opendj.ldap.ByteSequence;
-import org.forgerock.opendj.ldap.DecodeException;
-
-import com.forgerock.opendj.util.GeneralizedTime;
+import org.forgerock.opendj.ldap.GeneralizedTime;
 
 /**
  * This class implements the fax attribute syntax. This should be restricted to
@@ -85,10 +84,10 @@
     public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
             final LocalizableMessageBuilder invalidReason) {
         try {
-            GeneralizedTime.decode(value);
+            GeneralizedTime.valueOf(value.toString());
             return true;
-        } catch (final DecodeException de) {
-            invalidReason.append(de.getMessageObject());
+        } catch (final LocalizedIllegalArgumentException e) {
+            invalidReason.append(e.getMessageObject());
             return false;
         }
     }
diff --git a/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties b/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
index de8ee7b..b54b134 100755
--- a/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
+++ b/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -1399,3 +1399,7 @@
  entry does not exist 
 REJECTED_CHANGE_FAIL_MODIFYDN_DUPE=The entry "%s" could not be renamed because \
  there is already an entry with the same name
+FUNCTIONS_TO_INTEGER_FAIL=The provided value "%s" could not be parsed as an \
+ integer
+FUNCTIONS_TO_LONG_FAIL=The provided value "%s" could not be parsed as an \
+ long
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java
new file mode 100644
index 0000000..ce524df
--- /dev/null
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AttributeParserTestCase.java
@@ -0,0 +1,360 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2012 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.NoSuchElementException;
+
+import org.fest.util.Collections;
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.Test;
+
+/**
+ * Test {@code AttributeParser}.
+ */
+@SuppressWarnings("javadoc")
+public final class AttributeParserTestCase extends SdkTestCase {
+
+    @Test
+    public void testAsBooleanTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: true");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: false");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isFalse();
+    }
+
+    @Test
+    public void testAsBooleanTrueDefaultFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: true");
+        assertThat(e.parseAttribute("enabled").asBoolean(false)).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanFalseDefaultTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: false");
+        assertThat(e.parseAttribute("enabled").asBoolean(true)).isFalse();
+    }
+
+    @Test
+    public void testAsBooleanMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean()).isNull();
+    }
+
+    @Test
+    public void testAsBooleanMissingDefaultTrue() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean(true)).isTrue();
+    }
+
+    @Test
+    public void testAsBooleanMissingDefaultFalse() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("enabled").asBoolean(false)).isFalse();
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsBooleanMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("enabled").requireValue().asBoolean();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsBooleanInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "enabled: xxx");
+        e.parseAttribute("enabled").asBoolean();
+    }
+
+    @Test
+    public void testAsInteger99() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asInteger()).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsInteger99Default100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asInteger(100)).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsIntegerMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asInteger()).isNull();
+    }
+
+    @Test
+    public void testAsIntegerMissingDefault100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asInteger(100)).isEqualTo(100);
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsIntegerMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("age").requireValue().asInteger();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsIntegerInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: xxx");
+        e.parseAttribute("age").asInteger();
+    }
+
+    @Test
+    public void testAsLong99() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asLong()).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsLong99Default100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: 99");
+        assertThat(e.parseAttribute("age").asLong(100)).isEqualTo(99);
+    }
+
+    @Test
+    public void testAsLongMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asLong()).isNull();
+    }
+
+    @Test
+    public void testAsLongMissingDefault100() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("age").asLong(100)).isEqualTo(100);
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsLongMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("age").requireValue().asLong();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsLongInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "age: xxx");
+        e.parseAttribute("age").asLong();
+    }
+
+    @Test
+    public void testAsDN() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: cn=manager");
+        assertThat((Object) e.parseAttribute("manager").asDN()).isEqualTo(DN.valueOf("cn=manager"));
+    }
+
+    @Test
+    public void testAsDNDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: cn=manager");
+        assertThat((Object) e.parseAttribute("manager").asDN("cn=boss")).isEqualTo(
+                DN.valueOf("cn=manager"));
+    }
+
+    @Test
+    public void testAsDNMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("manager").asDN()).isNull();
+    }
+
+    @Test
+    public void testAsDNMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat((Object) e.parseAttribute("manager").asDN(DN.valueOf("cn=boss"))).isEqualTo(
+                DN.valueOf("cn=boss"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsDNMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("manager").requireValue().asDN();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsDNInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "manager: xxx");
+        e.parseAttribute("manager").asDN();
+    }
+
+    @Test
+    public void testAsAttributeDescription() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asAttributeDescription()).isEqualTo(
+                AttributeDescription.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsAttributeDescriptionDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asAttributeDescription("sn")).isEqualTo(
+                AttributeDescription.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsAttributeDescriptionMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asAttributeDescription()).isNull();
+    }
+
+    @Test
+    public void testAsAttributeDescriptionMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(
+                e.parseAttribute("type").asAttributeDescription(AttributeDescription.valueOf("sn")))
+                .isEqualTo(AttributeDescription.valueOf("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsAttributeDescriptionMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asAttributeDescription();
+    }
+
+    @Test(expectedExceptions = { IllegalArgumentException.class })
+    public void testAsAttributeDescriptionInvalid() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: ;x");
+        e.parseAttribute("type").asAttributeDescription();
+    }
+
+    @Test
+    public void testAsString() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asString()).isEqualTo(String.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsStringDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asString("sn")).isEqualTo(String.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsStringMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asString()).isNull();
+    }
+
+    @Test
+    public void testAsStringMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asString(String.valueOf("sn"))).isEqualTo(
+                String.valueOf("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsStringMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asString();
+    }
+
+    @Test
+    public void testAsByteString() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asByteString()).isEqualTo(ByteString.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsByteStringDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test", "type: cn");
+        assertThat(e.parseAttribute("type").asByteString(ByteString.valueOf("sn"))).isEqualTo(
+                ByteString.valueOf("cn"));
+    }
+
+    @Test
+    public void testAsByteStringMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asByteString()).isNull();
+    }
+
+    @Test
+    public void testAsByteStringMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        assertThat(e.parseAttribute("type").asByteString(ByteString.valueOf("sn"))).isEqualTo(
+                ByteString.valueOf("sn"));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsByteStringMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=test", "objectClass: test");
+        e.parseAttribute("type").requireValue().asByteString();
+    }
+
+    // Smoke test for set of methods: use one type only since the code is common
+    // and we've already tested the parsing.
+    @Test
+    public void testAsSetOfDN() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: cn=member2", "member: cn=member3");
+        assertThat(e.parseAttribute("member").asSetOfDN()).isEqualTo(
+                Collections.set(DN.valueOf("cn=member1"), DN.valueOf("cn=member2"), DN
+                        .valueOf("cn=member3")));
+    }
+
+    @Test
+    public void testAsSetOfDNDefault() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: cn=member2", "member: cn=member3");
+        assertThat(e.parseAttribute("member").asSetOfDN("cn=dummy1", "cn=dummy2")).isEqualTo(
+                Collections.set(DN.valueOf("cn=member1"), DN.valueOf("cn=member2"), DN
+                        .valueOf("cn=member3")));
+    }
+
+    @Test
+    public void testAsSetOfDNMissing() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        assertThat(e.parseAttribute("member").asSetOfDN()).isEqualTo(
+                java.util.Collections.emptySet());
+    }
+
+    @Test
+    public void testAsSetOfDNMissingDefault() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        assertThat(e.parseAttribute("member").asSetOfDN("cn=dummy1", "cn=dummy2")).isEqualTo(
+                Collections.set(DN.valueOf("cn=dummy1"), DN.valueOf("cn=dummy2")));
+    }
+
+    @Test(expectedExceptions = { NoSuchElementException.class })
+    public void testAsSetOfDNMissingRequired() {
+        Entry e = new LinkedHashMapEntry("dn: cn=group", "objectClass: group");
+        e.parseAttribute("member").requireValue().asSetOfDN();
+    }
+
+    @Test(expectedExceptions = { LocalizedIllegalArgumentException.class })
+    public void testAsSetOfDNInvalid() {
+        Entry e =
+                new LinkedHashMapEntry("dn: cn=group", "objectClass: group", "member: cn=member1",
+                        "member: xxxx");
+        e.parseAttribute("member").asSetOfDN();
+    }
+
+}
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java
new file mode 100644
index 0000000..19628a4
--- /dev/null
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/GeneralizedTimeTest.java
@@ -0,0 +1,132 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2009 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import static org.fest.assertions.Assertions.assertThat;
+
+/**
+ * Generalized time tests.
+ */
+@SuppressWarnings("javadoc")
+public class GeneralizedTimeTest extends SdkTestCase {
+
+    @DataProvider
+    public Object[][] validStrings() {
+        return new Object[][] { { "2006090613Z" }, { "20060906135030+01" }, { "200609061350Z" },
+            { "20060906135030Z" }, { "20061116135030Z" }, { "20061126135030Z" },
+            { "20061231235959Z" }, { "20060906135030+0101" }, { "20060906135030+2359" }, };
+    }
+
+    @DataProvider
+    public Object[][] invalidStrings() {
+        return new Object[][] { { "20060906135030+3359" }, { "20060906135030+2389" },
+            { "20060906135030+2361" }, { "20060906135030+" }, { "20060906135030+0" },
+            { "20060906135030+010" }, { "20061200235959Z" }, { "2006121a235959Z" },
+            { "2006122a235959Z" }, { "20060031235959Z" }, { "20061331235959Z" },
+            { "20062231235959Z" }, { "20061232235959Z" }, { "2006123123595aZ" },
+            { "200a1231235959Z" }, { "2006j231235959Z" }, { "200612-1235959Z" },
+            { "20061231#35959Z" }, { "2006" }, };
+    }
+
+    @Test(expectedExceptions = { LocalizedIllegalArgumentException.class },
+            dataProvider = "invalidStrings")
+    public void testValueOfInvalidString(String s) {
+        GeneralizedTime.valueOf(s);
+    }
+
+    @Test(dataProvider = "validStrings")
+    public void testValueOfValidString(String s) {
+        assertThat(GeneralizedTime.valueOf(s).toString()).isEqualTo(s);
+    }
+
+    @Test
+    public void testValueOfLong() {
+        Date date = new Date();
+        GeneralizedTime time = GeneralizedTime.valueOf(date.getTime());
+        assertThat(time.getTimeInMillis()).isEqualTo(date.getTime());
+        assertThat(time.toDate()).isEqualTo(date);
+    }
+
+    @Test
+    public void testValueOfDate() {
+        Date date = new Date();
+        GeneralizedTime time = GeneralizedTime.valueOf(date);
+        assertThat(time.getTimeInMillis()).isEqualTo(date.getTime());
+        assertThat(time.toDate()).isEqualTo(date);
+    }
+
+    @Test
+    public void testValueOfCalendar() {
+        Calendar calendar = Calendar.getInstance();
+        GeneralizedTime time = GeneralizedTime.valueOf(calendar);
+        assertThat(time.getTimeInMillis()).isEqualTo(calendar.getTimeInMillis());
+        assertThat(time.toCalendar()).isEqualTo(calendar);
+        assertThat(time.toDate()).isEqualTo(calendar.getTime());
+    }
+
+    @Test
+    public void testEqualsTrue() {
+        GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906125030Z");
+        assertThat(gt1).isEqualTo(gt2);
+    }
+
+    @Test
+    public void testEqualsFalse() {
+        GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030Z");
+        GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030+01");
+        assertThat(gt1).isNotEqualTo(gt2);
+    }
+
+    @Test
+    public void testCompareEquals() {
+        GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906125030Z");
+        assertThat(gt1.compareTo(gt2)).isEqualTo(0);
+    }
+
+    @Test
+    public void testCompareLessThan() {
+        GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030+01");
+        GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030Z");
+        assertThat(gt1.compareTo(gt2) < 0).isTrue();
+    }
+
+    @Test
+    public void testCompareGreaterThan() {
+        GeneralizedTime gt1 = GeneralizedTime.valueOf("20060906135030Z");
+        GeneralizedTime gt2 = GeneralizedTime.valueOf("20060906135030+01");
+        assertThat(gt1.compareTo(gt2) > 0).isTrue();
+    }
+
+}

--
Gitblit v1.10.0