/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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.
 */
package org.opends.server.schema;



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

import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.admin.std.server.AttributeSyntaxCfg;
import org.opends.server.api.ApproximateMatchingRule;
import org.opends.server.api.AttributeSyntax;
import org.opends.server.api.EqualityMatchingRule;
import org.opends.server.api.OrderingMatchingRule;
import org.opends.server.api.SubstringMatchingRule;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.*;

import static org.opends.messages.SchemaMessages.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.ServerConstants.*;



/**
 * 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>
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

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



  // The default equality matching rule for this syntax.
  private EqualityMatchingRule defaultEqualityMatchingRule;

  // The default ordering matching rule for this syntax.
  private OrderingMatchingRule defaultOrderingMatchingRule;

  // The default substring matching rule for this syntax.
  private SubstringMatchingRule defaultSubstringMatchingRule;



  /**
   * 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 void initializeSyntax(AttributeSyntaxCfg configuration)
         throws ConfigException
  {
    defaultEqualityMatchingRule =
         DirectoryServer.getEqualityMatchingRule(EMR_GENERALIZED_TIME_OID);
    if (defaultEqualityMatchingRule == null)
    {
      logError(ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE.get(
          EMR_GENERALIZED_TIME_OID, SYNTAX_GENERALIZED_TIME_NAME));
    }

    defaultOrderingMatchingRule =
         DirectoryServer.getOrderingMatchingRule(OMR_GENERALIZED_TIME_OID);
    if (defaultOrderingMatchingRule == null)
    {
      logError(ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE.get(
          OMR_GENERALIZED_TIME_OID, SYNTAX_GENERALIZED_TIME_NAME));
    }

    defaultSubstringMatchingRule =
         DirectoryServer.getSubstringMatchingRule(SMR_CASE_IGNORE_OID);
    if (defaultSubstringMatchingRule == null)
    {
      logError(ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE.get(
          SMR_CASE_IGNORE_OID, SYNTAX_GENERALIZED_TIME_NAME));
    }
  }



  /**
   * Retrieves the common name for this attribute syntax.
   *
   * @return  The common name for this attribute syntax.
   */
  @Override
  public String getSyntaxName()
  {
    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 default equality matching rule that will be used for
   * attributes with this syntax.
   *
   * @return  The default equality matching rule that will be used for
   *          attributes with this syntax, or <CODE>null</CODE> if equality
   *          matches will not be allowed for this type by default.
   */
  @Override
  public EqualityMatchingRule getEqualityMatchingRule()
  {
    return defaultEqualityMatchingRule;
  }



  /**
   * Retrieves the default ordering matching rule that will be used for
   * attributes with this syntax.
   *
   * @return  The default ordering matching rule that will be used for
   *          attributes with this syntax, or <CODE>null</CODE> if ordering
   *          matches will not be allowed for this type by default.
   */
  @Override
  public OrderingMatchingRule getOrderingMatchingRule()
  {
    return defaultOrderingMatchingRule;
  }



  /**
   * Retrieves the default substring matching rule that will be used for
   * attributes with this syntax.
   *
   * @return  The default substring matching rule that will be used for
   *          attributes with this syntax, or <CODE>null</CODE> if substring
   *          matches will not be allowed for this type by default.
   */
  @Override
  public SubstringMatchingRule getSubstringMatchingRule()
  {
    return defaultSubstringMatchingRule;
  }



  /**
   * Retrieves the default approximate matching rule that will be used for
   * attributes with this syntax.
   *
   * @return  The default approximate matching rule that will be used for
   *          attributes with this syntax, or <CODE>null</CODE> if approximate
   *          matches will not be allowed for this type by default.
   */
  @Override
  public ApproximateMatchingRule getApproximateMatchingRule()
  {
    // Approximate matching will not be allowed by default.
    return null;
  }



  /**
   * Indicates whether the provided value is acceptable for use in an attribute
   * with this syntax.  If it is not, then the reason may be appended to the
   * provided buffer.
   *
   * @param  value          The value for which to make the determination.
   * @param  invalidReason  The buffer to which the invalid reason should be
   *                        appended.
   *
   * @return  <CODE>true</CODE> if the provided value is acceptable for use with
   *          this syntax, or <CODE>false</CODE> if not.
   */
  @Override
  public boolean valueIsAcceptable(ByteSequence value,
                                   MessageBuilder invalidReason)
  {
    try
    {
      decodeGeneralizedTimeValue(value);
      return true;
    }
    catch (DirectoryException de)
    {
      invalidReason.append(de.getMessageObject());
      return false;
    }
  }



  /**
   * 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 AttributeValue createGeneralizedTimeValue(long time)
  {
    String valueString = format(time);
    return AttributeValues.create(ByteString.valueOf(valueString),
        ByteString.valueOf(valueString));
  }



  /**
   * 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)
    {
      Message 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:
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_YEAR.get(
              valueString, String.valueOf(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:
            Message 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:
            Message message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_MONTH.get(valueString,
                                        valueString.substring(4, 6));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;
      default:
        Message 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:
            Message 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:
            Message 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:
            Message 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:
            Message message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_DAY.get(valueString,
                                        valueString.substring(6, 8));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      default:
        Message 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:
            Message 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:
            Message 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:
            Message message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_HOUR.get(valueString,
                                        valueString.substring(8, 10));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

      default:
        Message 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)
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(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:
            Message 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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, String.valueOf(e));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

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

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

      default:
        Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
            valueString, String.valueOf(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)
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(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:
            Message 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)
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(s1), 12);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
        }

        if (valueString.charAt(13) != '0')
        {
          Message 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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, String.valueOf(e));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

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

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

      default:
        Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
            valueString, String.valueOf(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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            // This should only happen if the provided date wasn't legal
            // (e.g., September 31).
            Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.
                get(valueString, String.valueOf(e));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
        else
        {
          Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
              valueString, String.valueOf(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)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

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

      default:
        Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_CHAR.get(
            valueString, String.valueOf(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))
          {
            Message message =
                WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.
                  get(value, String.valueOf(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:
          Message message =
              WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_FRACTION_CHAR.
                get(value, String.valueOf(c));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message);
      }
    }

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

    if (timeZone == null)
    {
      Message 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)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      // This should only happen if the provided date wasn't legal
      // (e.g., September 31).
      Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_ILLEGAL_TIME.get(
          value, String.valueOf(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);
    if ((offSetStr.length() != 3) && (offSetStr.length() != 5))
    {
      Message 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:
        Message 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:
            Message 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:
            Message message = WARN_ATTR_SYNTAX_GENERALIZED_TIME_INVALID_OFFSET.
                get(value, offSetStr);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message);
        }
        break;

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

        default:
          Message 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);
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isBinary()
  {
    return false;
  }
}

