From 046430396dd40a63d6f574bf53c4d3ad2977a130 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Mon, 07 May 2012 16:48:34 +0000
Subject: [PATCH] Initial implementation for OPENDJ-355: Add fluent API for decoding attributes

---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java                                  |   14 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java                                          |   28 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java            |   10 
 opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java                                | 1299 ++++++++++++++++++++++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java                              |    7 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java                                     |    8 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java    |    9 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java                        |   15 
 opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/Functions.java                                      |   46 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attribute.java                                      |    8 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeEqualityMatchingRuleImpl.java |    5 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java                                |  589 ++++++++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeOrderingMatchingRuleImpl.java |    5 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java                |   15 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/GeneralizedTimeSyntaxImpl.java               | 1136 --------------------
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java                                        |   14 
 16 files changed, 2,076 insertions(+), 1,132 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 8fb41d3..64c21f8 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
@@ -22,13 +22,18 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
  */
 
 package com.forgerock.opendj.util;
 
+import java.util.Calendar;
+
+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.schema.Schema;
 
 /**
@@ -64,6 +69,14 @@
                 }
             };
 
+    private static final Function<ByteString, String, Void> BYTESTRING_TO_BASE64 =
+            new Function<ByteString, String, Void>() {
+
+                public String apply(final ByteString value, final Void p) {
+                    return Base64.encode(value);
+                }
+            };
+
     private static final Function<ByteString, Boolean, Void> BYTESTRING_TO_BOOLEAN =
             new Function<ByteString, Boolean, Void>() {
 
@@ -83,6 +96,18 @@
                 }
             };
 
+    private static final Function<ByteString, Calendar, Void> BYTESTRING_TO_CALENDAR =
+            new Function<ByteString, Calendar, Void>() {
+
+                public Calendar apply(final ByteString value, final Void p) {
+                    try {
+                        return GeneralizedTime.decode(value);
+                    } catch (DecodeException e) {
+                        throw new LocalizedIllegalArgumentException(e.getMessageObject(), e);
+                    }
+                }
+            };
+
     private static final Function<ByteString, DN, Schema> BYTESTRING_TO_DN =
             new Function<ByteString, DN, Schema>() {
 
@@ -236,6 +261,15 @@
     }
 
     /**
+     * Returns a function which encodes a {@code ByteString} as {@code Base64}.
+     *
+     * @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},
@@ -251,6 +285,18 @@
 
     /**
      * 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}.
+     *
+     * @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}.
      *
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java b/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java
new file mode 100644
index 0000000..953e92a
--- /dev/null
+++ b/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/GeneralizedTime.java
@@ -0,0 +1,1299 @@
+/*
+ * 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 com.forgerock.opendj.util;
+
+import static org.forgerock.opendj.ldap.CoreMessages.*;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+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;
+
+/**
+ * Utility class for encoding and decoding generalized time syntax values.
+ */
+public final class 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}.
+     *
+     * @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.
+     */
+    public static Calendar decode(final ByteSequence value) throws DecodeException {
+        int year = 0;
+        int month = 0;
+        int day = 0;
+        int hour = 0;
+        int minute = 0;
+        int second = 0;
+
+        // 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 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;
+        }
+
+        // The first four characters are the century and year, and they must
+        // be numeric digits between 0 and 9.
+        for (int i = 0; i < 4; i++) {
+            switch (valueString.charAt(i)) {
+            case '0':
+                year = year * 10;
+                break;
+
+            case '1':
+                year = (year * 10) + 1;
+                break;
+
+            case '2':
+                year = (year * 10) + 2;
+                break;
+
+            case '3':
+                year = (year * 10) + 3;
+                break;
+
+            case '4':
+                year = (year * 10) + 4;
+                break;
+
+            case '5':
+                year = (year * 10) + 5;
+                break;
+
+            case '6':
+                year = (year * 10) + 6;
+                break;
+
+            case '7':
+                year = (year * 10) + 7;
+                break;
+
+            case '8':
+                year = (year * 10) + 8;
+                break;
+
+            case '9':
+                year = (year * 10) + 9;
+                break;
+
+            default:
+                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;
+            }
+        }
+
+        // The next two characters are the month, and they must form the
+        // string representation of an integer between 01 and 12.
+        char m1 = valueString.charAt(4);
+        final char m2 = valueString.charAt(5);
+        switch (m1) {
+        case '0':
+            // m2 must be a digit between 1 and 9.
+            switch (m2) {
+            case '1':
+                month = Calendar.JANUARY;
+                break;
+
+            case '2':
+                month = Calendar.FEBRUARY;
+                break;
+
+            case '3':
+                month = Calendar.MARCH;
+                break;
+
+            case '4':
+                month = Calendar.APRIL;
+                break;
+
+            case '5':
+                month = Calendar.MAY;
+                break;
+
+            case '6':
+                month = Calendar.JUNE;
+                break;
+
+            case '7':
+                month = Calendar.JULY;
+                break;
+
+            case '8':
+                month = Calendar.AUGUST;
+                break;
+
+            case '9':
+                month = Calendar.SEPTEMBER;
+                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;
+            }
+            break;
+        case '1':
+            // m2 must be a digit between 0 and 2.
+            switch (m2) {
+            case '0':
+                month = Calendar.OCTOBER;
+                break;
+
+            case '1':
+                month = Calendar.NOVEMBER;
+                break;
+
+            case '2':
+                month = Calendar.DECEMBER;
+                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;
+            }
+            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;
+        }
+
+        // The next two characters should be the day of the month, and they
+        // must form the string representation of an integer between 01 and
+        // 31. This doesn't do any validation against the year or month, so
+        // it will allow dates like April 31, or February 29 in a non-leap
+        // year, but we'll let those slide.
+        final char d1 = valueString.charAt(6);
+        final char d2 = valueString.charAt(7);
+        switch (d1) {
+        case '0':
+            // d2 must be a digit between 1 and 9.
+            switch (d2) {
+            case '1':
+                day = 1;
+                break;
+
+            case '2':
+                day = 2;
+                break;
+
+            case '3':
+                day = 3;
+                break;
+
+            case '4':
+                day = 4;
+                break;
+
+            case '5':
+                day = 5;
+                break;
+
+            case '6':
+                day = 6;
+                break;
+
+            case '7':
+                day = 7;
+                break;
+
+            case '8':
+                day = 8;
+                break;
+
+            case '9':
+                day = 9;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        case '1':
+            // d2 must be a digit between 0 and 9.
+            switch (d2) {
+            case '0':
+                day = 10;
+                break;
+
+            case '1':
+                day = 11;
+                break;
+
+            case '2':
+                day = 12;
+                break;
+
+            case '3':
+                day = 13;
+                break;
+
+            case '4':
+                day = 14;
+                break;
+
+            case '5':
+                day = 15;
+                break;
+
+            case '6':
+                day = 16;
+                break;
+
+            case '7':
+                day = 17;
+                break;
+
+            case '8':
+                day = 18;
+                break;
+
+            case '9':
+                day = 19;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        case '2':
+            // d2 must be a digit between 0 and 9.
+            switch (d2) {
+            case '0':
+                day = 20;
+                break;
+
+            case '1':
+                day = 21;
+                break;
+
+            case '2':
+                day = 22;
+                break;
+
+            case '3':
+                day = 23;
+                break;
+
+            case '4':
+                day = 24;
+                break;
+
+            case '5':
+                day = 25;
+                break;
+
+            case '6':
+                day = 26;
+                break;
+
+            case '7':
+                day = 27;
+                break;
+
+            case '8':
+                day = 28;
+                break;
+
+            case '9':
+                day = 29;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        case '3':
+            // d2 must be either 0 or 1.
+            switch (d2) {
+            case '0':
+                day = 30;
+                break;
+
+            case '1':
+                day = 31;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        default:
+            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;
+        }
+
+        // The next two characters must be the hour, and they must form the
+        // string representation of an integer between 00 and 23.
+        final char h1 = valueString.charAt(8);
+        final char h2 = valueString.charAt(9);
+        switch (h1) {
+        case '0':
+            switch (h2) {
+            case '0':
+                hour = 0;
+                break;
+
+            case '1':
+                hour = 1;
+                break;
+
+            case '2':
+                hour = 2;
+                break;
+
+            case '3':
+                hour = 3;
+                break;
+
+            case '4':
+                hour = 4;
+                break;
+
+            case '5':
+                hour = 5;
+                break;
+
+            case '6':
+                hour = 6;
+                break;
+
+            case '7':
+                hour = 7;
+                break;
+
+            case '8':
+                hour = 8;
+                break;
+
+            case '9':
+                hour = 9;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        case '1':
+            switch (h2) {
+            case '0':
+                hour = 10;
+                break;
+
+            case '1':
+                hour = 11;
+                break;
+
+            case '2':
+                hour = 12;
+                break;
+
+            case '3':
+                hour = 13;
+                break;
+
+            case '4':
+                hour = 14;
+                break;
+
+            case '5':
+                hour = 15;
+                break;
+
+            case '6':
+                hour = 16;
+                break;
+
+            case '7':
+                hour = 17;
+                break;
+
+            case '8':
+                hour = 18;
+                break;
+
+            case '9':
+                hour = 19;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        case '2':
+            switch (h2) {
+            case '0':
+                hour = 20;
+                break;
+
+            case '1':
+                hour = 21;
+                break;
+
+            case '2':
+                hour = 22;
+                break;
+
+            case '3':
+                hour = 23;
+                break;
+
+            default:
+                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;
+            }
+            break;
+
+        default:
+            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;
+        }
+
+        // Next, there should be either two digits comprising an integer
+        // between 00 and 59 (for the minute), a letter 'Z' (for the UTC
+        // specifier), a plus or minus sign followed by two or four digits
+        // (for the UTC offset), or a period or comma representing the
+        // fraction.
+        m1 = valueString.charAt(10);
+        switch (m1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 13) {
+                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;
+            }
+
+            minute = 10 * (m1 - '0');
+
+            switch (valueString.charAt(11)) {
+            case '0':
+                break;
+
+            case '1':
+                minute += 1;
+                break;
+
+            case '2':
+                minute += 2;
+                break;
+
+            case '3':
+                minute += 3;
+                break;
+
+            case '4':
+                minute += 4;
+                break;
+
+            case '5':
+                minute += 5;
+                break;
+
+            case '6':
+                minute += 6;
+                break;
+
+            case '7':
+                minute += 7;
+                break;
+
+            case '8':
+                minute += 8;
+                break;
+
+            case '9':
+                minute += 9;
+                break;
+
+            default:
+                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;
+            }
+
+            break;
+
+        case 'Z':
+            // This is fine only if we are at the end of the value.
+            if (length == 11) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if ((length == 13) || (length == 15)) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 10);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 11, year, month, day, hour, minute, second,
+                    3600000);
+
+        default:
+            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;
+        }
+
+        // Next, there should be either two digits comprising an integer
+        // between 00 and 60 (for the second, including a possible leap
+        // second), a letter 'Z' (for the UTC specifier), a plus or minus
+        // sign followed by two or four digits (for the UTC offset), or a
+        // period or comma to start the fraction.
+        final char s1 = valueString.charAt(12);
+        switch (s1) {
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+            // There must be at least two more characters, and the next one
+            // must be a digit between 0 and 9.
+            if (length < 15) {
+                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;
+            }
+
+            second = 10 * (s1 - '0');
+
+            switch (valueString.charAt(13)) {
+            case '0':
+                break;
+
+            case '1':
+                second += 1;
+                break;
+
+            case '2':
+                second += 2;
+                break;
+
+            case '3':
+                second += 3;
+                break;
+
+            case '4':
+                second += 4;
+                break;
+
+            case '5':
+                second += 5;
+                break;
+
+            case '6':
+                second += 6;
+                break;
+
+            case '7':
+                second += 7;
+                break;
+
+            case '8':
+                second += 8;
+                break;
+
+            case '9':
+                second += 9;
+                break;
+
+            default:
+                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;
+            }
+
+            break;
+
+        case '6':
+            // There must be at least two more characters and the next one
+            // must be a 0.
+            if (length < 15) {
+                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;
+            }
+
+            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;
+            }
+
+            second = 60;
+            break;
+
+        case 'Z':
+            // This is fine only if we are at the end of the value.
+            if (length == 13) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if ((length == 15) || (length == 17)) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 12);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 13, year, month, day, hour, minute, second,
+                    60000);
+
+        default:
+            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;
+        }
+
+        // Next, there should be either a period or comma followed by
+        // between one and three digits (to specify the sub-second), a
+        // letter 'Z' (for the UTC specifier), or a plus or minus sign
+        // followed by two our four digits (for the UTC offset).
+        switch (valueString.charAt(14)) {
+        case '.':
+        case ',':
+            return finishDecodingFraction(valueString, 15, year, month, day, hour, minute, second,
+                    1000);
+
+        case 'Z':
+            // This is fine only if we are at the end of the value.
+            if (length == 15) {
+                final TimeZone tz = TIME_ZONE_UTC_OBJ;
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        case '+':
+        case '-':
+            // These are fine only if there are exactly two or four more
+            // digits that specify a valid offset.
+            if ((length == 17) || (length == 19)) {
+                final TimeZone tz = getTimeZoneForOffset(valueString, 14);
+                return createTime(valueString, year, month, day, hour, minute, second, tz);
+            } else {
+                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;
+            }
+
+        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;
+        }
+    }
+
+    /**
+     * 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
+     * parameters.
+     *
+     * @param value
+     *            The generalized time string representation.
+     * @param year
+     *            The year.
+     * @param month
+     *            The month.
+     * @param day
+     *            The day.
+     * @param hour
+     *            The hour.
+     * @param minute
+     *            The minute.
+     * @param second
+     *            The second.
+     * @param tz
+     *            The timezone.
+     * @return A Calendar object representing the provided date / time
+     *         parameters.
+     * @throws DecodeException
+     *             If the calendar 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 {
+        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;
+        } 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;
+        }
+    }
+
+    /**
+     * Completes decoding the generalized time value containing a fractional
+     * component. It will also decode the trailing 'Z' or offset.
+     *
+     * @param value
+     *            The whole value, including the fractional component and time
+     *            zone information.
+     * @param startPos
+     *            The position of the first character after the period in the
+     *            value string.
+     * @param year
+     *            The year decoded from the provided value.
+     * @param month
+     *            The month decoded from the provided value.
+     * @param day
+     *            The day decoded from the provided value.
+     * @param hour
+     *            The hour decoded from the provided value.
+     * @param minute
+     *            The minute decoded from the provided value.
+     * @param second
+     *            The second decoded from the provided value.
+     * @param multiplier
+     *            The multiplier value that should be used to scale the fraction
+     *            appropriately. If it's a fraction of an hour, then it should
+     *            be 3600000 (60*60*1000). If it's a fraction of a minute, then
+     *            it should be 60000. If it's a fraction of a second, then it
+     *            should be 1000.
+     * @return The timestamp created from the provided generalized time value
+     *         including the fractional element.
+     * @throws DecodeException
+     *             If the provided value cannot be parsed as a valid generalized
+     *             time string.
+     */
+    private static Calendar 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 length = value.length();
+        final StringBuilder fractionBuffer = new StringBuilder((2 + length) - startPos);
+        fractionBuffer.append("0.");
+
+        TimeZone timeZone = null;
+
+    outerLoop:
+        for (int i = startPos; i < length; i++) {
+            final char c = value.charAt(i);
+            switch (c) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                fractionBuffer.append(c);
+                break;
+
+            case 'Z':
+                // This is only acceptable if we're at the end of the value.
+                if (i != (value.length() - 1)) {
+                    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;
+                }
+
+                timeZone = TIME_ZONE_UTC_OBJ;
+                break outerLoop;
+
+            case '+':
+            case '-':
+                timeZone = getTimeZoneForOffset(value, i);
+                break outerLoop;
+
+            default:
+                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;
+            }
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        final Double fractionValue = Double.parseDouble(fractionBuffer.toString());
+        final int additionalMilliseconds = (int) Math.round(fractionValue * multiplier);
+
+        try {
+            final GregorianCalendar calendar = new GregorianCalendar();
+            calendar.setLenient(false);
+            calendar.setTimeZone(timeZone);
+            calendar.set(year, month, day, hour, minute, second);
+            calendar.set(Calendar.MILLISECOND, additionalMilliseconds);
+            return calendar;
+        } 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;
+        }
+    }
+
+    /**
+     * Decodes a time zone offset from the provided value.
+     *
+     * @param value
+     *            The whole value, including the offset.
+     * @param startPos
+     *            The position of the first character that is contained in the
+     *            offset. This should be the position of the plus or minus
+     *            character.
+     * @return The {@code TimeZone} object representing the decoded time zone.
+     * @throws DecodeException
+     *             If the provided value does not contain a valid offset.
+     */
+    private static TimeZone getTimeZoneForOffset(final String value, final int startPos)
+            throws DecodeException {
+        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;
+        }
+
+        // The first character must be either a plus or minus.
+        switch (offSetStr.charAt(0)) {
+        case '+':
+        case '-':
+            // These are OK.
+            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;
+        }
+
+        // The first two characters must be an integer between 00 and 23.
+        switch (offSetStr.charAt(1)) {
+        case '0':
+        case '1':
+            switch (offSetStr.charAt(2)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // These are all fine.
+                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;
+            }
+            break;
+
+        case '2':
+            switch (offSetStr.charAt(2)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+                // These are all fine.
+                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;
+            }
+            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;
+        }
+
+        // If there are two more characters, then they must be an integer
+        // between 00 and 59.
+        if (offSetStr.length() == 5) {
+            switch (offSetStr.charAt(3)) {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+                switch (offSetStr.charAt(4)) {
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    // These are all fine.
+                    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;
+                }
+                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;
+            }
+        }
+
+        // If we've gotten here, then it looks like a valid offset. We can
+        // create a time zone by using "GMT" followed by the offset.
+        return TimeZone.getTimeZone("GMT" + offSetStr);
+    }
+
+    private GeneralizedTime() {
+        // Prevent instantiation.
+    }
+
+}
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java
index 81dec63..2c2a2c3 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractAttribute.java
@@ -265,6 +265,13 @@
     /**
      * {@inheritDoc}
      */
+    public AttributeParser parse() {
+        return AttributeParser.parseAttribute(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public abstract Iterator<ByteString> iterator();
 
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
index 37664b5..9aa3359 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractEntry.java
@@ -234,6 +234,20 @@
     /**
      * {@inheritDoc}
      */
+    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        return AttributeParser.parseAttribute(getAttribute(attributeDescription));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public AttributeParser parseAttribute(String attributeDescription) {
+        return AttributeParser.parseAttribute(getAttribute(attributeDescription));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     public boolean removeAttribute(final AttributeDescription attributeDescription) {
         return removeAttribute(Attributes.emptyAttribute(attributeDescription), null);
     }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attribute.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attribute.java
index 2ef52e0..931e041 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attribute.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attribute.java
@@ -256,6 +256,14 @@
     Iterator<ByteString> iterator();
 
     /**
+     * Returns a parser for this attribute which can be used for decoding values
+     * as different types of object.
+     *
+     * @return A parser for this attribute.
+     */
+    AttributeParser parse();
+
+    /**
      * Removes {@code value} from this attribute if it is present (optional
      * operation). If this attribute does not contain {@code value}, the call
      * leaves the attribute unchanged and returns {@code false}.
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
new file mode 100644
index 0000000..c34923a
--- /dev/null
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeParser.java
@@ -0,0 +1,589 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+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;
+
+/**
+ * A fluent API for parsing attributes as different types of value. An attribute
+ * parser is obtained from an entry using the method
+ * {@link Entry#parseAttribute} or from an attribute using
+ * {@link Attribute#parse()}.
+ * <p>
+ * Methods throw an {@code IllegalArgumentException} when a value cannot be
+ * parsed (e.g. because its syntax is invalid). Methods which return a
+ * {@code Set} always return a modifiable non-{@code null} result.
+ * <p>
+ * Examples:
+ *
+ * <pre>
+ * Entry entry = ...;
+ *
+ * Calendar timestamp = entry.parseAttribute("createTimestamp").asCalendar();
+ * boolean isEnabled = entry.parseAttribute("enabled").asBoolean(false);
+ *
+ * Entry group = ...;
+ * Schema schema = ...;
+ *
+ * Set&lt;DN&gt; members = group.parseAttribute("member").usingSchema(schema).asSetOfDN();
+ * </pre>
+ *
+ * @see Entry#parseAttribute
+ * @see Attribute#parse()
+ */
+public final class AttributeParser {
+    // TODO: enums, filters, rdns?
+
+    private static final AttributeParser NULL_INSTANCE = new AttributeParser(null);
+
+    /**
+     * Returns an attribute parser for the provided attribute. {@code null}
+     * attributes are permitted and will be treated as if an empty attribute was
+     * provided.
+     *
+     * @param attribute
+     *            The attribute to be parsed, which may be {@code null}.
+     * @return The attribute parser.
+     */
+    public static AttributeParser parseAttribute(final Attribute attribute) {
+        return isEmpty(attribute) ? NULL_INSTANCE : new AttributeParser(attribute);
+    }
+
+    private static boolean isEmpty(final Attribute attribute) {
+        return (attribute == null) || attribute.isEmpty();
+    }
+
+    private final Attribute attribute;
+    private Schema schema;
+
+    private AttributeParser(final Attribute attribute) {
+        this.attribute = attribute;
+    }
+
+    /**
+     * Returns the first value decoded as an {@code AttributeDescription} using
+     * the schema associated with this parser, or {@code null} if the attribute
+     * does not contain any values.
+     *
+     * @return The first value decoded as an {@code AttributeDescription}.
+     */
+    public AttributeDescription asAttributeDescription() {
+        return asAttributeDescription(null);
+    }
+
+    /**
+     * 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 decoded as an {@code AttributeDescription}.
+     */
+    public AttributeDescription asAttributeDescription(final AttributeDescription defaultValue) {
+        return parseSingleValue(Functions.valueToAttributeDescription(getSchema()), defaultValue);
+    }
+
+    /**
+     * 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
+     * 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.
+     */
+    public String asBase64(final ByteString defaultValue) {
+        return parseSingleValue(Functions.valueToBase64(), Base64.encode(defaultValue));
+    }
+
+    /**
+     * Returns the first value decoded as a boolean, or {@code null} if the
+     * attribute does not contain any values.
+     *
+     * @return The first value decoded as a boolean.
+     */
+    public Boolean asBoolean() {
+        return isEmpty(attribute) ? null : asBoolean(false /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Boolean}, 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 Boolean}.
+     */
+    public boolean asBoolean(final boolean defaultValue) {
+        return parseSingleValue(Functions.valueToBoolean(), defaultValue);
+    }
+
+    /**
+     * Returns the first value, or {@code null} if the attribute does not
+     * contain any values.
+     *
+     * @return The first value.
+     */
+    public ByteString asByteString() {
+        return asByteString(null);
+    }
+
+    /**
+     * Returns the first value, 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.
+     */
+    public ByteString asByteString(final ByteString defaultValue) {
+        return parseSingleValue(Functions.<ByteString> identityFunction(), defaultValue);
+    }
+
+    /**
+     * 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.
+     *
+     * @return The first value decoded as a {@code DN}.
+     */
+    public DN asDN() {
+        return asDN(null);
+    }
+
+    /**
+     * 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 DN defaultValue) {
+        return parseSingleValue(Functions.valueToDN(getSchema()), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Integer}, or {@code null} if
+     * the attribute does not contain any values.
+     *
+     * @return The first value decoded as an {@code Integer}.
+     */
+    public Integer asInteger() {
+        return isEmpty(attribute) ? null : asInteger(0 /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as an {@code Integer}, 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 Integer}.
+     */
+    public int asInteger(final int defaultValue) {
+        return parseSingleValue(Functions.valueToInteger(), defaultValue);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code Long}, or {@code null} if the
+     * attribute does not contain any values.
+     *
+     * @return The first value decoded as a {@code Long}.
+     */
+    public Long asLong() {
+        return isEmpty(attribute) ? null : asLong(0L /* ignored */);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code Long}, 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 Long}.
+     */
+    public long asLong(final long defaultValue) {
+        return parseSingleValue(Functions.valueToLong(), defaultValue);
+    }
+
+    /**
+     * 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 decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription(
+            final AttributeDescription... defaultValues) {
+        return asSetOfAttributeDescription(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * 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 decoded as a set of {@code AttributeDescription}s.
+     */
+    public Set<AttributeDescription> asSetOfAttributeDescription(
+            final Collection<AttributeDescription> defaultValues) {
+        return parseMultipleValues(Functions.valueToAttributeDescription(), defaultValues);
+    }
+
+    /**
+     * 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 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));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Boolean}s, 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 Boolean}s.
+     */
+    public Set<Boolean> asSetOfBoolean(final Boolean... defaultValues) {
+        return asSetOfBoolean(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Boolean}s, 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 Boolean}s.
+     */
+    public Set<Boolean> asSetOfBoolean(final Collection<Boolean> defaultValues) {
+        return parseMultipleValues(Functions.valueToBoolean(), defaultValues);
+    }
+
+    /**
+     * Returns the values contained in the attribute, 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.
+     */
+    public Set<ByteString> asSetOfByteString(final ByteString... defaultValues) {
+        return asSetOfByteString(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values contained in the attribute, 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.
+     */
+    public Set<ByteString> asSetOfByteString(final Collection<ByteString> defaultValues) {
+        return parseMultipleValues(Functions.<ByteString> identityFunction(), 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 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);
+    }
+
+    /**
+     * 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 Collection<DN> defaultValues) {
+        return parseMultipleValues(Functions.valueToDN(), defaultValues);
+    }
+
+    /**
+     * 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 DN... defaultValues) {
+        return asSetOfDN(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.
+     *
+     * @param defaultValues
+     *            The default values to return if the attribute is empty.
+     * @return The values decoded as a set of {@code Integer}s.
+     */
+    public Set<Integer> asSetOfInteger(final Collection<Integer> defaultValues) {
+        return parseMultipleValues(Functions.valueToInteger(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Integer}s, 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 Integer}s.
+     */
+    public Set<Integer> asSetOfInteger(final Integer... defaultValues) {
+        return asSetOfInteger(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Long}s, 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 Long}s.
+     */
+    public Set<Long> asSetOfLong(final Collection<Long> defaultValues) {
+        return parseMultipleValues(Functions.valueToLong(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code Long}s, 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 Long}s.
+     */
+    public Set<Long> asSetOfLong(final Long... defaultValues) {
+        return asSetOfLong(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code String}s, 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 String}s.
+     */
+    public Set<String> asSetOfString(final Collection<String> defaultValues) {
+        return parseMultipleValues(Functions.valueToString(), defaultValues);
+    }
+
+    /**
+     * Returns the values decoded as a set of {@code String}s, 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 String}s.
+     */
+    public Set<String> asSetOfString(final String... defaultValues) {
+        return asSetOfString(Arrays.asList(defaultValues));
+    }
+
+    /**
+     * Returns the first value decoded as a {@code String}, or {@code null} if
+     * the attribute does not contain any values.
+     *
+     * @return The first value decoded as a {@code String}.
+     */
+    public String asString() {
+        return asString(null);
+    }
+
+    /**
+     * Returns the first value decoded as a {@code String}, 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 String}.
+     */
+    public String asString(final String defaultValue) {
+        return parseSingleValue(Functions.valueToString(), defaultValue);
+    }
+
+    /**
+     * Throws a {@code NoSuchElementException} if the attribute referenced by
+     * this parser is {@code null} or empty.
+     *
+     * @return A reference to this attribute parser.
+     * @throws NoSuchElementException
+     *             If the attribute referenced by this parser is {@code null} or
+     *             empty.
+     */
+    public AttributeParser requireValue() {
+        if (isEmpty(attribute)) {
+            throw new NoSuchElementException();
+        } else {
+            return this;
+        }
+    }
+
+    /**
+     * Sets the {@code Schema} which will be used when parsing schema sensitive
+     * values such as DNs and attribute descriptions.
+     *
+     * @param schema
+     *            The {@code Schema} which will be used when parsing schema
+     *            sensitive values.
+     * @return This attribute parser.
+     */
+    public AttributeParser usingSchema(final Schema schema) {
+        // Avoid modifying the null instance: a schema will not be needed
+        // anyway.
+        if (this != NULL_INSTANCE) {
+            this.schema = schema;
+        }
+        return this;
+    }
+
+    private Schema getSchema() {
+        return schema == null ? Schema.getDefaultSchema() : schema;
+    }
+
+    private <T> Set<T> parseMultipleValues(final Function<ByteString, T, ?> f,
+            final Collection<? extends T> defaultValues) {
+        if (!isEmpty(attribute)) {
+            final LinkedHashSet<T> result = new LinkedHashSet<T>(attribute.size());
+            for (final ByteString b : attribute) {
+                result.add(f.apply(b, null));
+            }
+            return result;
+        } else if (defaultValues != null) {
+            return new LinkedHashSet<T>(defaultValues);
+        } else {
+            return new LinkedHashSet<T>(0);
+        }
+    }
+
+    private <T> T parseSingleValue(final Function<ByteString, T, ?> f, final T defaultValue) {
+        if (!isEmpty(attribute)) {
+            return f.apply(attribute.firstValue(), null);
+        } else {
+            return defaultValue;
+        }
+    }
+}
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
index 34f707c..75e044f 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
@@ -163,6 +163,10 @@
             return AbstractAttribute.hashCode(this);
         }
 
+        public AttributeParser parse() {
+            return attribute.parse();
+        }
+
         public boolean isEmpty() {
             return attribute.isEmpty();
         }
@@ -286,6 +290,10 @@
             return Iterators.unmodifiableIterator(attribute.iterator());
         }
 
+        public AttributeParser parse() {
+            return attribute.parse();
+        }
+
         public boolean remove(final Object value) {
             throw new UnsupportedOperationException();
         }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
index 1c6f67f..afa54b4 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
@@ -180,6 +180,20 @@
         /**
          * {@inheritDoc}
          */
+        public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+            return entry.parseAttribute(attributeDescription);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public AttributeParser parseAttribute(String attributeDescription) {
+            return entry.parseAttribute(attributeDescription);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
         @Override
         public boolean removeAttribute(final Attribute attribute,
                 final Collection<ByteString> missingValues) {
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
index 0873122..3c1c382 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entry.java
@@ -323,6 +323,34 @@
     int hashCode();
 
     /**
+     * Returns a parser for the named attribute contained in this entry.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be parsed.
+     * @return A parser for the named attribute.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    AttributeParser parseAttribute(AttributeDescription attributeDescription);
+
+    /**
+     * Returns a parser for the named attribute contained in this entry.
+     * <p>
+     * The attribute description will be decoded using the schema associated
+     * with this entry (usually the default schema).
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be parsed.
+     * @return A parser for the named attribute.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code attributeDescription} could not be decoded using
+     *             the schema associated with this entry.
+     * @throws NullPointerException
+     *             If {@code attributeDescription} was {@code null}.
+     */
+    AttributeParser parseAttribute(String attributeDescription);
+
+    /**
      * Removes all of the attribute values contained in {@code attribute} from
      * this entry if it is present (optional operation). If {@code attribute} is
      * empty then the entire attribute will be removed if it is present.
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java
index 35b24eb..97c8a6c 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AddRequestImpl.java
@@ -31,6 +31,7 @@
 
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
@@ -263,6 +264,20 @@
     /**
      * {@inheritDoc}
      */
+    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public AttributeParser parseAttribute(String attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public boolean equals(final Object object) {
         return entry.equals(object);
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java
index 8a85be2..9f68d7a 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableAddRequestImpl.java
@@ -22,6 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -30,6 +31,7 @@
 
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
 import org.forgerock.opendj.ldap.Attributes;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
@@ -125,6 +127,14 @@
         return impl.getName();
     }
 
+    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    public AttributeParser parseAttribute(String attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
     public boolean removeAttribute(Attribute attribute, Collection<ByteString> missingValues) {
         throw new UnsupportedOperationException();
     }
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java
index f528a7f..6d9372b 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/SearchResultEntryImpl.java
@@ -31,6 +31,7 @@
 
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
@@ -172,6 +173,20 @@
     /**
      * {@inheritDoc}
      */
+    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public AttributeParser parseAttribute(String attributeDescription) {
+        return entry.parseAttribute(attributeDescription);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     public boolean removeAttribute(final Attribute attribute,
             final Collection<ByteString> missingValues) {
         return entry.removeAttribute(attribute, missingValues);
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java
index d74f02f..5accd75 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/UnmodifiableSearchResultEntryImpl.java
@@ -31,6 +31,7 @@
 
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AttributeParser;
 import org.forgerock.opendj.ldap.Attributes;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
@@ -121,6 +122,14 @@
         return impl.getName();
     }
 
+    public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
+    public AttributeParser parseAttribute(String attributeDescription) {
+        return impl.parseAttribute(attributeDescription);
+    }
+
     public boolean removeAttribute(Attribute attribute, Collection<ByteString> missingValues) {
         throw new UnsupportedOperationException();
     }
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 6ff75c1..e7191f6 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
@@ -22,6 +22,7 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap.schema;
 
@@ -29,6 +30,8 @@
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DecodeException;
 
+import com.forgerock.opendj.util.GeneralizedTime;
+
 /**
  * This class defines the generalizedTimeMatch matching rule defined in X.520
  * and referenced in RFC 2252.
@@ -36,6 +39,6 @@
 final class GeneralizedTimeEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
     public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
             throws DecodeException {
-        return ByteString.valueOf(GeneralizedTimeSyntaxImpl.decodeGeneralizedTimeValue(value));
+        return ByteString.valueOf(GeneralizedTime.decode(value).getTimeInMillis());
     }
 }
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 fba9ef9..c7cb533 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
@@ -22,6 +22,7 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
+ *      Portions copyright 2012 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap.schema;
 
@@ -29,6 +30,8 @@
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DecodeException;
 
+import com.forgerock.opendj.util.GeneralizedTime;
+
 /**
  * This class defines the generalizedTimeOrderingMatch matching rule defined in
  * X.520 and referenced in RFC 2252.
@@ -36,6 +39,6 @@
 final class GeneralizedTimeOrderingMatchingRuleImpl extends AbstractOrderingMatchingRuleImpl {
     public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value)
             throws DecodeException {
-        return ByteString.valueOf(GeneralizedTimeSyntaxImpl.decodeGeneralizedTimeValue(value));
+        return ByteString.valueOf(GeneralizedTime.decode(value).getTimeInMillis());
     }
 }
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 21d9d8b..1b28a5b 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
@@ -27,19 +27,16 @@
 
 package org.forgerock.opendj.ldap.schema;
 
-import static org.forgerock.opendj.ldap.CoreMessages.*;
-import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_GENERALIZED_TIME_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.OMR_GENERALIZED_TIME_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SMR_CASE_IGNORE_OID;
+import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_GENERALIZED_TIME_NAME;
 
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-
-import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizableMessageBuilder;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.DecodeException;
 
-import com.forgerock.opendj.util.StaticUtils;
+import com.forgerock.opendj.util.GeneralizedTime;
 
 /**
  * This class implements the fax attribute syntax. This should be restricted to
@@ -48,1127 +45,6 @@
  */
 final class GeneralizedTimeSyntaxImpl extends AbstractSyntaxImpl {
 
-    // UTC TimeZone is assumed to never change over JVM lifetime
-    private static final TimeZone TIME_ZONE_UTC_OBJ = TimeZone.getTimeZone(TIME_ZONE_UTC);
-
-    /**
-     * Decodes the provided normalized value as a generalized time value and
-     * retrieves a timestamp containing its representation.
-     *
-     * @param value
-     *            The normalized value to decode using the generalized time
-     *            syntax.
-     * @return The timestamp created from the provided generalized time value.
-     * @throws DecodeException
-     *             If the provided value cannot be parsed as a valid generalized
-     *             time string.
-     */
-    static long decodeGeneralizedTimeValue(final ByteSequence value) throws DecodeException {
-        int year = 0;
-        int month = 0;
-        int day = 0;
-        int hour = 0;
-        int minute = 0;
-        int second = 0;
-
-        // 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 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;
-        }
-
-        // The first four characters are the century and year, and they must
-        // be numeric digits between 0 and 9.
-        for (int i = 0; i < 4; i++) {
-            switch (valueString.charAt(i)) {
-            case '0':
-                year = year * 10;
-                break;
-
-            case '1':
-                year = year * 10 + 1;
-                break;
-
-            case '2':
-                year = year * 10 + 2;
-                break;
-
-            case '3':
-                year = year * 10 + 3;
-                break;
-
-            case '4':
-                year = year * 10 + 4;
-                break;
-
-            case '5':
-                year = year * 10 + 5;
-                break;
-
-            case '6':
-                year = year * 10 + 6;
-                break;
-
-            case '7':
-                year = year * 10 + 7;
-                break;
-
-            case '8':
-                year = year * 10 + 8;
-                break;
-
-            case '9':
-                year = year * 10 + 9;
-                break;
-
-            default:
-                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;
-            }
-        }
-
-        // The next two characters are the month, and they must form the
-        // string representation of an integer between 01 and 12.
-        char m1 = valueString.charAt(4);
-        final char m2 = valueString.charAt(5);
-        switch (m1) {
-        case '0':
-            // m2 must be a digit between 1 and 9.
-            switch (m2) {
-            case '1':
-                month = Calendar.JANUARY;
-                break;
-
-            case '2':
-                month = Calendar.FEBRUARY;
-                break;
-
-            case '3':
-                month = Calendar.MARCH;
-                break;
-
-            case '4':
-                month = Calendar.APRIL;
-                break;
-
-            case '5':
-                month = Calendar.MAY;
-                break;
-
-            case '6':
-                month = Calendar.JUNE;
-                break;
-
-            case '7':
-                month = Calendar.JULY;
-                break;
-
-            case '8':
-                month = Calendar.AUGUST;
-                break;
-
-            case '9':
-                month = Calendar.SEPTEMBER;
-                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;
-            }
-            break;
-        case '1':
-            // m2 must be a digit between 0 and 2.
-            switch (m2) {
-            case '0':
-                month = Calendar.OCTOBER;
-                break;
-
-            case '1':
-                month = Calendar.NOVEMBER;
-                break;
-
-            case '2':
-                month = Calendar.DECEMBER;
-                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;
-            }
-            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;
-        }
-
-        // The next two characters should be the day of the month, and they
-        // must form the string representation of an integer between 01 and
-        // 31. This doesn't do any validation against the year or month, so
-        // it will allow dates like April 31, or February 29 in a non-leap
-        // year, but we'll let those slide.
-        final char d1 = valueString.charAt(6);
-        final char d2 = valueString.charAt(7);
-        switch (d1) {
-        case '0':
-            // d2 must be a digit between 1 and 9.
-            switch (d2) {
-            case '1':
-                day = 1;
-                break;
-
-            case '2':
-                day = 2;
-                break;
-
-            case '3':
-                day = 3;
-                break;
-
-            case '4':
-                day = 4;
-                break;
-
-            case '5':
-                day = 5;
-                break;
-
-            case '6':
-                day = 6;
-                break;
-
-            case '7':
-                day = 7;
-                break;
-
-            case '8':
-                day = 8;
-                break;
-
-            case '9':
-                day = 9;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        case '1':
-            // d2 must be a digit between 0 and 9.
-            switch (d2) {
-            case '0':
-                day = 10;
-                break;
-
-            case '1':
-                day = 11;
-                break;
-
-            case '2':
-                day = 12;
-                break;
-
-            case '3':
-                day = 13;
-                break;
-
-            case '4':
-                day = 14;
-                break;
-
-            case '5':
-                day = 15;
-                break;
-
-            case '6':
-                day = 16;
-                break;
-
-            case '7':
-                day = 17;
-                break;
-
-            case '8':
-                day = 18;
-                break;
-
-            case '9':
-                day = 19;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        case '2':
-            // d2 must be a digit between 0 and 9.
-            switch (d2) {
-            case '0':
-                day = 20;
-                break;
-
-            case '1':
-                day = 21;
-                break;
-
-            case '2':
-                day = 22;
-                break;
-
-            case '3':
-                day = 23;
-                break;
-
-            case '4':
-                day = 24;
-                break;
-
-            case '5':
-                day = 25;
-                break;
-
-            case '6':
-                day = 26;
-                break;
-
-            case '7':
-                day = 27;
-                break;
-
-            case '8':
-                day = 28;
-                break;
-
-            case '9':
-                day = 29;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        case '3':
-            // d2 must be either 0 or 1.
-            switch (d2) {
-            case '0':
-                day = 30;
-                break;
-
-            case '1':
-                day = 31;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        default:
-            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;
-        }
-
-        // The next two characters must be the hour, and they must form the
-        // string representation of an integer between 00 and 23.
-        final char h1 = valueString.charAt(8);
-        final char h2 = valueString.charAt(9);
-        switch (h1) {
-        case '0':
-            switch (h2) {
-            case '0':
-                hour = 0;
-                break;
-
-            case '1':
-                hour = 1;
-                break;
-
-            case '2':
-                hour = 2;
-                break;
-
-            case '3':
-                hour = 3;
-                break;
-
-            case '4':
-                hour = 4;
-                break;
-
-            case '5':
-                hour = 5;
-                break;
-
-            case '6':
-                hour = 6;
-                break;
-
-            case '7':
-                hour = 7;
-                break;
-
-            case '8':
-                hour = 8;
-                break;
-
-            case '9':
-                hour = 9;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        case '1':
-            switch (h2) {
-            case '0':
-                hour = 10;
-                break;
-
-            case '1':
-                hour = 11;
-                break;
-
-            case '2':
-                hour = 12;
-                break;
-
-            case '3':
-                hour = 13;
-                break;
-
-            case '4':
-                hour = 14;
-                break;
-
-            case '5':
-                hour = 15;
-                break;
-
-            case '6':
-                hour = 16;
-                break;
-
-            case '7':
-                hour = 17;
-                break;
-
-            case '8':
-                hour = 18;
-                break;
-
-            case '9':
-                hour = 19;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        case '2':
-            switch (h2) {
-            case '0':
-                hour = 20;
-                break;
-
-            case '1':
-                hour = 21;
-                break;
-
-            case '2':
-                hour = 22;
-                break;
-
-            case '3':
-                hour = 23;
-                break;
-
-            default:
-                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;
-            }
-            break;
-
-        default:
-            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;
-        }
-
-        // Next, there should be either two digits comprising an integer
-        // between 00 and 59 (for the minute), a letter 'Z' (for the UTC
-        // specifier), a plus or minus sign followed by two or four digits
-        // (for the UTC offset), or a period or comma representing the
-        // fraction.
-        m1 = valueString.charAt(10);
-        switch (m1) {
-        case '0':
-        case '1':
-        case '2':
-        case '3':
-        case '4':
-        case '5':
-            // There must be at least two more characters, and the next one
-            // must be a digit between 0 and 9.
-            if (length < 13) {
-                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;
-            }
-
-            minute = 10 * (m1 - '0');
-
-            switch (valueString.charAt(11)) {
-            case '0':
-                break;
-
-            case '1':
-                minute += 1;
-                break;
-
-            case '2':
-                minute += 2;
-                break;
-
-            case '3':
-                minute += 3;
-                break;
-
-            case '4':
-                minute += 4;
-                break;
-
-            case '5':
-                minute += 5;
-                break;
-
-            case '6':
-                minute += 6;
-                break;
-
-            case '7':
-                minute += 7;
-                break;
-
-            case '8':
-                minute += 8;
-                break;
-
-            case '9':
-                minute += 9;
-                break;
-
-            default:
-                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;
-            }
-
-            break;
-
-        case 'Z':
-            // This is fine only if we are at the end of the value.
-            if (length == 11) {
-                final TimeZone tz = TIME_ZONE_UTC_OBJ;
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        case '+':
-        case '-':
-            // These are fine only if there are exactly two or four more
-            // digits that specify a valid offset.
-            if (length == 13 || length == 15) {
-                final TimeZone tz = getTimeZoneForOffset(valueString, 10);
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        case '.':
-        case ',':
-            return finishDecodingFraction(valueString, 11, year, month, day, hour, minute, second,
-                    3600000);
-
-        default:
-            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;
-        }
-
-        // Next, there should be either two digits comprising an integer
-        // between 00 and 60 (for the second, including a possible leap
-        // second), a letter 'Z' (for the UTC specifier), a plus or minus
-        // sign followed by two or four digits (for the UTC offset), or a
-        // period or comma to start the fraction.
-        final char s1 = valueString.charAt(12);
-        switch (s1) {
-        case '0':
-        case '1':
-        case '2':
-        case '3':
-        case '4':
-        case '5':
-            // There must be at least two more characters, and the next one
-            // must be a digit between 0 and 9.
-            if (length < 15) {
-                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;
-            }
-
-            second = 10 * (s1 - '0');
-
-            switch (valueString.charAt(13)) {
-            case '0':
-                break;
-
-            case '1':
-                second += 1;
-                break;
-
-            case '2':
-                second += 2;
-                break;
-
-            case '3':
-                second += 3;
-                break;
-
-            case '4':
-                second += 4;
-                break;
-
-            case '5':
-                second += 5;
-                break;
-
-            case '6':
-                second += 6;
-                break;
-
-            case '7':
-                second += 7;
-                break;
-
-            case '8':
-                second += 8;
-                break;
-
-            case '9':
-                second += 9;
-                break;
-
-            default:
-                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;
-            }
-
-            break;
-
-        case '6':
-            // There must be at least two more characters and the next one
-            // must be a 0.
-            if (length < 15) {
-                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;
-            }
-
-            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;
-            }
-
-            second = 60;
-            break;
-
-        case 'Z':
-            // This is fine only if we are at the end of the value.
-            if (length == 13) {
-                final TimeZone tz = TIME_ZONE_UTC_OBJ;
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        case '+':
-        case '-':
-            // These are fine only if there are exactly two or four more
-            // digits that specify a valid offset.
-            if (length == 15 || length == 17) {
-                final TimeZone tz = getTimeZoneForOffset(valueString, 12);
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        case '.':
-        case ',':
-            return finishDecodingFraction(valueString, 13, year, month, day, hour, minute, second,
-                    60000);
-
-        default:
-            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;
-        }
-
-        // Next, there should be either a period or comma followed by
-        // between one and three digits (to specify the sub-second), a
-        // letter 'Z' (for the UTC specifier), or a plus or minus sign
-        // followed by two our four digits (for the UTC offset).
-        switch (valueString.charAt(14)) {
-        case '.':
-        case ',':
-            return finishDecodingFraction(valueString, 15, year, month, day, hour, minute, second,
-                    1000);
-
-        case 'Z':
-            // This is fine only if we are at the end of the value.
-            if (length == 15) {
-                final TimeZone tz = TIME_ZONE_UTC_OBJ;
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        case '+':
-        case '-':
-            // These are fine only if there are exactly two or four more
-            // digits that specify a valid offset.
-            if (length == 17 || length == 19) {
-                final TimeZone tz = getTimeZoneForOffset(valueString, 14);
-                return createTime(valueString, year, month, day, hour, minute, second, tz);
-            } else {
-                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;
-            }
-
-        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;
-        }
-    }
-
-    private static long createTime(final String value, int year, int month, int day, int hour,
-            int minute, int second, final TimeZone tz) throws DecodeException {
-        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.getTimeInMillis();
-        } 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;
-        }
-    }
-
-    /**
-     * Completes decoding the generalized time value containing a fractional
-     * component. It will also decode the trailing 'Z' or offset.
-     *
-     * @param value
-     *            The whole value, including the fractional component and time
-     *            zone information.
-     * @param startPos
-     *            The position of the first character after the period in the
-     *            value string.
-     * @param year
-     *            The year decoded from the provided value.
-     * @param month
-     *            The month decoded from the provided value.
-     * @param day
-     *            The day decoded from the provided value.
-     * @param hour
-     *            The hour decoded from the provided value.
-     * @param minute
-     *            The minute decoded from the provided value.
-     * @param second
-     *            The second decoded from the provided value.
-     * @param multiplier
-     *            The multiplier value that should be used to scale the fraction
-     *            appropriately. If it's a fraction of an hour, then it should
-     *            be 3600000 (60*60*1000). If it's a fraction of a minute, then
-     *            it should be 60000. If it's a fraction of a second, then it
-     *            should be 1000.
-     * @return The timestamp created from the provided generalized time value
-     *         including the fractional element.
-     * @throws DecodeException
-     *             If the provided value cannot be parsed as a valid generalized
-     *             time string.
-     */
-    private static long 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 length = value.length();
-        final StringBuilder fractionBuffer = new StringBuilder(2 + length - startPos);
-        fractionBuffer.append("0.");
-
-        TimeZone timeZone = null;
-
-    outerLoop:
-        for (int i = startPos; i < length; i++) {
-            final char c = value.charAt(i);
-            switch (c) {
-            case '0':
-            case '1':
-            case '2':
-            case '3':
-            case '4':
-            case '5':
-            case '6':
-            case '7':
-            case '8':
-            case '9':
-                fractionBuffer.append(c);
-                break;
-
-            case 'Z':
-                // This is only acceptable if we're at the end of the value.
-                if (i != value.length() - 1) {
-                    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;
-                }
-
-                timeZone = TIME_ZONE_UTC_OBJ;
-                break outerLoop;
-
-            case '+':
-            case '-':
-                timeZone = getTimeZoneForOffset(value, i);
-                break outerLoop;
-
-            default:
-                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;
-            }
-        }
-
-        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;
-        }
-
-        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;
-        }
-
-        final Double fractionValue = Double.parseDouble(fractionBuffer.toString());
-        final long additionalMilliseconds = Math.round(fractionValue * multiplier);
-
-        try {
-            final GregorianCalendar calendar = new GregorianCalendar();
-            calendar.setLenient(false);
-            calendar.setTimeZone(timeZone);
-            calendar.set(year, month, day, hour, minute, second);
-            calendar.set(Calendar.MILLISECOND, 0);
-            return calendar.getTimeInMillis() + additionalMilliseconds;
-        } 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;
-        }
-    }
-
-    /**
-     * Decodes a time zone offset from the provided value.
-     *
-     * @param value
-     *            The whole value, including the offset.
-     * @param startPos
-     *            The position of the first character that is contained in the
-     *            offset. This should be the position of the plus or minus
-     *            character.
-     * @return The {@code TimeZone} object representing the decoded time zone.
-     * @throws DecodeException
-     *             If the provided value does not contain a valid offset.
-     */
-    private static TimeZone getTimeZoneForOffset(final String value, final int startPos)
-            throws DecodeException {
-        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;
-        }
-
-        // The first character must be either a plus or minus.
-        switch (offSetStr.charAt(0)) {
-        case '+':
-        case '-':
-            // These are OK.
-            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;
-        }
-
-        // The first two characters must be an integer between 00 and 23.
-        switch (offSetStr.charAt(1)) {
-        case '0':
-        case '1':
-            switch (offSetStr.charAt(2)) {
-            case '0':
-            case '1':
-            case '2':
-            case '3':
-            case '4':
-            case '5':
-            case '6':
-            case '7':
-            case '8':
-            case '9':
-                // These are all fine.
-                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;
-            }
-            break;
-
-        case '2':
-            switch (offSetStr.charAt(2)) {
-            case '0':
-            case '1':
-            case '2':
-            case '3':
-                // These are all fine.
-                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;
-            }
-            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;
-        }
-
-        // If there are two more characters, then they must be an integer
-        // between 00 and 59.
-        if (offSetStr.length() == 5) {
-            switch (offSetStr.charAt(3)) {
-            case '0':
-            case '1':
-            case '2':
-            case '3':
-            case '4':
-            case '5':
-                switch (offSetStr.charAt(4)) {
-                case '0':
-                case '1':
-                case '2':
-                case '3':
-                case '4':
-                case '5':
-                case '6':
-                case '7':
-                case '8':
-                case '9':
-                    // These are all fine.
-                    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;
-                }
-                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;
-            }
-        }
-
-        // If we've gotten here, then it looks like a valid offset. We can
-        // create a time zone by using "GMT" followed by the offset.
-        return TimeZone.getTimeZone("GMT" + offSetStr);
-    }
-
     @Override
     public String getEqualityMatchingRule() {
         return EMR_GENERALIZED_TIME_OID;
@@ -1209,7 +85,7 @@
     public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,
             final LocalizableMessageBuilder invalidReason) {
         try {
-            decodeGeneralizedTimeValue(value);
+            GeneralizedTime.decode(value);
             return true;
         } catch (final DecodeException de) {
             invalidReason.append(de.getMessageObject());

--
Gitblit v1.10.0