/*
 * 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 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2009 D. J. Hagberg, Millibits Consulting, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS
 */
package org.opends.server.schema;

import static org.opends.messages.SchemaMessages.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.ServerConstants.*;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.schema.Syntax;
import org.opends.server.admin.std.server.AttributeSyntaxCfg;
import org.opends.server.api.AttributeSyntax;
import org.opends.server.types.DirectoryException;

/**
 * This class defines the generalized time attribute syntax, which is a way of
 * representing time in a form like "YYYYMMDDhhmmssZ".  The actual form is
 * somewhat flexible, and may omit the minute and second information, or may
 * include sub-second information.  It may also replace "Z" with a time zone
 * offset like "-0500" for representing values that are not in UTC.
 */
public class GeneralizedTimeSyntax
       extends AttributeSyntax<AttributeSyntaxCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** UTC TimeZone is assumed to never change over JVM lifetime. */
  private static final TimeZone TIME_ZONE_UTC_OBJ =
      TimeZone.getTimeZone(TIME_ZONE_UTC);

  /**
   * Creates a new instance of this syntax.  Note that the only thing that
   * should be done here is to invoke the default constructor for the
   * superclass.  All initialization should be performed in the
   * <CODE>initializeSyntax</CODE> method.
   */
  public GeneralizedTimeSyntax()
  {
    super();
  }

  /** {@inheritDoc} */
  @Override
  public Syntax getSDKSyntax(Schema schema)
  {
    return schema.getSyntax(SchemaConstants.SYNTAX_GENERALIZED_TIME_OID);
  }

  /**
   * Retrieves the common name for this attribute syntax.
   *
   * @return  The common name for this attribute syntax.
   */
  @Override
  public String getName()
  {
    return SYNTAX_GENERALIZED_TIME_NAME;
  }

  /**
   * Retrieves the OID for this attribute syntax.
   *
   * @return  The OID for this attribute syntax.
   */
  @Override
  public String getOID()
  {
    return SYNTAX_GENERALIZED_TIME_OID;
  }

  /**
   * Retrieves a description for this attribute syntax.
   *
   * @return  A description for this attribute syntax.
   */
  @Override
  public String getDescription()
  {
    return SYNTAX_GENERALIZED_TIME_DESCRIPTION;
  }

  /**
   * Retrieves the generalized time representation of the provided date.
   *
   * @param  d  The date to retrieve in generalized time form.
   *
   * @return  The generalized time representation of the provided date.
   */
  public static String format(Date d)
  {
    return d == null ? null : format(d.getTime());
  }

  /**
   * Retrieves the generalized time representation of the provided date.
   *
   * @param  t  The timestamp to retrieve in generalized time form.
   *
   * @return  The generalized time representation of the provided date.
   */
  public static String format(long t)
  {
    // 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.

    StringBuilder sb = new StringBuilder(19);

    GregorianCalendar calendar = new GregorianCalendar(TIME_ZONE_UTC_OBJ);
    calendar.setLenient(false);
    calendar.setTimeInMillis(t);

    // 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 sb.toString();
  }

  /**
   * Retrieves an attribute value containing a generalized time representation
   * of the provided date.
   *
   * @param  time  The time for which to retrieve the generalized time value.
   *
   * @return  The attribute value created from the date.
   */
  public static ByteString createGeneralizedTimeValue(long time)
  {
    return ByteString.valueOfUtf8(format(time));
  }

  /**
   * 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  DirectoryException  If the provided value cannot be parsed as a
   *                              valid generalized time string.
   */
  public static long decodeGeneralizedTimeValue(ByteSequence value)
         throws DirectoryException
  {
    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.
    String valueString = value.toString().toUpperCase();
    int    length      = valueString.length();
    if (length < 11)
    {
      LocalizableMessage message =
          WARN_ATTR_SYNTAX_GENERALIZED_TIME_TOO_SHORT.get(valueString);
      throw new DirectoryException(
              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
    }


    // 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:
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR.get(
              valueString, valueString.charAt(i));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
      }
    }


    // 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);
    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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                        valueString.substring(4, 6));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                        valueString.substring(4, 6));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;
      default:
        LocalizableMessage message =
            WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                    valueString.substring(4, 6));
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }


    // 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.
    char d1 = valueString.charAt(6);
    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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                        valueString.substring(6, 8));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                        valueString.substring(6, 8));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                        valueString.substring(6, 8));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      case '3':
        // d2 must be either 0 or 1.
        switch (d2)
        {
          case '0':
            day = 30;
            break;

          case '1':
            day = 31;
            break;

          default:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                        valueString.substring(6, 8));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      default:
        LocalizableMessage message =
            WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                    valueString.substring(6, 8));
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }


    // The next two characters must be the hour, and they must form the string
    // representation of an integer between 00 and 23.
    char h1 = valueString.charAt(8);
    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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString,
                                        valueString.substring(8, 10));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString,
                                        valueString.substring(8, 10));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        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:
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString,
                                        valueString.substring(8, 10));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      default:
        LocalizableMessage message =
            WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString,
                                    valueString.substring(8, 10));
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }


    // 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)
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
        }


        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:
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE.
                get(valueString,
                                        valueString.substring(10, 12));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }

        break;

      case 'Z':
        // This is fine only if we are at the end of the value.
        if (length == 11)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(TIME_ZONE_UTC_OBJ);
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
        }

      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)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(getTimeZoneForOffset(valueString, 10));
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
        }

      case '.':
      case ',':
        return finishDecodingFraction(valueString, 11, year, month, day, hour,
                                      minute, second, 3600000);

      default:
        LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, m1, 10);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
    }


    // 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.
    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)
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
        }


        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:
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MINUTE.
                get(valueString,
                                        valueString.substring(12, 14));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }

        break;

      case '6':
        // There must be at least two more characters and the next one must be
        // a 0.
        if (length < 15)
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

        if (valueString.charAt(13) != '0')
        {
          LocalizableMessage message =
              WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_SECOND.get(valueString,
                                      valueString.substring(12, 14));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

        second = 60;
        break;

      case 'Z':
        // This is fine only if we are at the end of the value.
        if (length == 13)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(TIME_ZONE_UTC_OBJ);
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

      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)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(getTimeZoneForOffset(valueString, 12));
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

      case '.':
      case ',':
        return finishDecodingFraction(valueString, 13, year, month, day, hour,
                                      minute, second, 60000);

      default:
        LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(valueString, s1, 12);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }


    // 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)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(TIME_ZONE_UTC_OBJ);
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, valueString.charAt(14), 14);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

      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)
        {
          try
          {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setLenient(false);
            calendar.setTimeZone(getTimeZoneForOffset(valueString, 14));
            calendar.set(year, month, day, hour, minute, second);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTimeInMillis();
          }
          catch (Exception e)
          {
            logger.traceException(e);

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, e);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, valueString.charAt(14), 14);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

      default:
        LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
            valueString, valueString.charAt(14), 14);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }
  }

  /**
   * 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  DirectoryException  If the provided value cannot be parsed as a
   *                              valid generalized time string.
   */
  private static long finishDecodingFraction(String value, int startPos,
                                             int year, int month, int day,
                                             int hour, int minute, int second,
                                             int multiplier)
          throws DirectoryException
  {
    int length = value.length();
    StringBuilder fractionBuffer = new StringBuilder(2 + length - startPos);
    fractionBuffer.append("0.");

    TimeZone timeZone = null;

outerLoop:
    for (int i=startPos; i < length; i++)
    {
      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)
          {
            LocalizableMessage message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.
                  get(value, c);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
          }

          timeZone = TIME_ZONE_UTC_OBJ;
          break outerLoop;

        case '+':
        case '-':
          timeZone = getTimeZoneForOffset(value, i);
          break outerLoop;

        default:
          LocalizableMessage message =
              WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.
                get(value, c);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
      }
    }

    if (fractionBuffer.length() == 2)
    {
      LocalizableMessage message =
          WARN_ATTR_SYNTAX_GENERALIZED_TIME_EMPTY_FRACTION.get(value);
      throw new DirectoryException(
              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
    }

    if (timeZone == null)
    {
      LocalizableMessage message =
          WARN_ATTR_SYNTAX_GENERALIZED_TIME_NO_TIME_ZONE_INFO.get(value);
      throw new DirectoryException(
              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
    }

    Double fractionValue = Double.parseDouble(fractionBuffer.toString());
    long additionalMilliseconds = Math.round(fractionValue * multiplier);

    try
    {
      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 (Exception e)
    {
      logger.traceException(e);

      // This should only happen if the provided date wasn't legal
      // (e.g., September 31).
      LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(value, e);
      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                   message, e);
    }
  }

  /**
   * 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  DirectoryException  If the provided value does not contain a valid
   *                              offset.
   */
  private static TimeZone getTimeZoneForOffset(String value, int startPos)
          throws DirectoryException
  {
    String offSetStr = value.substring(startPos);
    int len = offSetStr.length();
    if (len != 3 && len != 5)
    {
      LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(
          value, offSetStr);
      throw new DirectoryException(
              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
    }


    // The first character must be either a plus or minus.
    switch (offSetStr.charAt(0))
    {
      case '+':
      case '-':
        // These are OK.
        break;

      default:
        LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(
            value, offSetStr);
        throw new DirectoryException(
                ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                message);
    }


    // 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:
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.
                get(value, offSetStr);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      case '2':
        switch (offSetStr.charAt(2))
        {
          case '0':
          case '1':
          case '2':
          case '3':
            // These are all fine.
            break;

          default:
            LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.
                get(value, offSetStr);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      default:
        LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.get(
            value, offSetStr);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                     message);
    }


    // If there are two more characters, then they must be an integer between
    // 00 and 59.
    if (len == 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:
              LocalizableMessage message =
                  WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.
                    get(value, offSetStr);
              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                           message);
          }
          break;

        default:
          LocalizableMessage message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.
              get(value, offSetStr);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
      }
    }


    // 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);
  }
}

