/*
 * 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-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.util;

import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.util.ServerConstants.*;

import java.io.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.LocalizableMessageDescriptor;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.Reject;
import org.opends.messages.ToolMessages;
import org.opends.server.api.ClientConnection;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
import org.opends.server.types.*;

import com.forgerock.opendj.cli.Argument;
import com.forgerock.opendj.cli.ArgumentException;

/**
 * This class defines a number of static utility methods that may be used
 * throughout the server.  Note that because of the frequency with which these
 * methods are expected to be used, very little debug logging will be performed
 * to prevent the log from filling up with unimportant calls and to reduce the
 * impact that debugging may have on performance.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
     mayInstantiate=false,
     mayExtend=false,
     mayInvoke=true)
public final class StaticUtils
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The number of bytes of a Java int. A Java int is 32 bits, i.e. 4 bytes. */
  public static final int INT_SIZE = 4;
  /** The number of bytes of a Java long. A Java int is 64 bits, i.e. 8 bytes. */
  public static final int LONG_SIZE = 8;

  /**
   * Number of bytes in a Kibibyte.
   * <p>
   * Example usage:
   * <pre>
   * int _10KB = 10 * KB;
   * </pre>
   */
  public static final int KB = 1024;
  /**
   * Number of bytes in a Mebibyte.
   * <p>
   * Example usage:
   * <pre>
   * int _10MB = 10 * MB;
   * </pre>
   */
  public static final int MB = KB * KB;

  /** Private constructor to prevent instantiation. */
  private StaticUtils() {
    // No implementation required.
  }

  /**
   * Construct a byte array containing the UTF-8 encoding of the
   * provided string. This is significantly faster
   * than calling {@link String#getBytes(String)} for ASCII strings.
   *
   * @param s
   *          The string to convert to a UTF-8 byte array.
   * @return Returns a byte array containing the UTF-8 encoding of the
   *         provided string.
   */
  public static byte[] getBytes(String s)
  {
    return com.forgerock.opendj.util.StaticUtils.getBytes(s);
  }


  /**
   * Returns the provided byte array decoded as a UTF-8 string without throwing
   * an UnsupportedEncodingException. This method is equivalent to:
   *
   * <pre>
   * try
   * {
   *   return new String(bytes, &quot;UTF-8&quot;);
   * }
   * catch (UnsupportedEncodingException e)
   * {
   *   // Should never happen: UTF-8 is always supported.
   *   throw new RuntimeException(e);
   * }
   * </pre>
   *
   * @param bytes
   *          The byte array to be decoded as a UTF-8 string.
   * @return The decoded string.
   */
  public static String decodeUTF8(final byte[] bytes)
  {
    Reject.ifNull(bytes);

    if (bytes.length == 0)
    {
      return "".intern();
    }

    final StringBuilder builder = new StringBuilder(bytes.length);
    final int sz = bytes.length;

    for (int i = 0; i < sz; i++)
    {
      final byte b = bytes[i];
      if ((b & 0x7f) != b)
      {
        try
        {
          builder.append(new String(bytes, i, (sz - i), "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
          // Should never happen: UTF-8 is always supported.
          throw new RuntimeException(e);
        }
        break;
      }
      builder.append((char) b);
    }
    return builder.toString();
  }



  /**
   * Retrieves a string representation of the provided byte in hexadecimal.
   *
   * @param b   The byte for which to retrieve the hexadecimal string
   *            representation.
   * @return The string representation of the provided byte in hexadecimal.
   */

  public static String byteToHex(final byte b)
  {
    return com.forgerock.opendj.util.StaticUtils.byteToHex(b);
  }
  /**
   * Retrieves a string representation of the provided byte in hexadecimal.
   *
   * @param  b  The byte for which to retrieve the hexadecimal string
   *            representation.
   * @return The string representation of the provided byte in hexadecimal
   *         using lowercase characters.
   */
  public static String byteToLowerHex(final byte b)
  {
    return com.forgerock.opendj.util.StaticUtils.byteToLowerHex(b);
  }

  /**
   * Retrieves a string representation of the contents of the provided byte
   * array using hexadecimal characters with no space between each byte.
   *
   * @param  b  The byte array containing the data.
   *
   * @return  A string representation of the contents of the provided byte
   *          array using hexadecimal characters.
   */
  public static String bytesToHexNoSpace(byte[] b)
  {
    if (b == null || b.length == 0)
    {
      return "";
    }

    int arrayLength = b.length;
    StringBuilder buffer = new StringBuilder(arrayLength * 2);

    for (int i=0; i < arrayLength; i++)
    {
      buffer.append(byteToHex(b[i]));
    }

    return buffer.toString();
  }



  /**
   * Retrieves a string representation of the contents of the provided byte
   * array using hexadecimal characters and a space between each byte.
   *
   * @param  b  The byte array containing the data.
   * @return  A string representation of the contents of the provided byte
   *          array using hexadecimal characters.
   */
  public static String bytesToHex(byte[] b)
  {
    if (b == null || b.length == 0)
    {
      return "";
    }

    int arrayLength = b.length;
    StringBuilder buffer = new StringBuilder((arrayLength - 1) * 3 + 2);
    buffer.append(byteToHex(b[0]));

    for (int i=1; i < arrayLength; i++)
    {
      buffer.append(" ");
      buffer.append(byteToHex(b[i]));
    }

    return buffer.toString();
  }

  /**
   * Retrieves a string representation of the contents of the provided byte
   * sequence using hexadecimal characters and a space between each byte.
   *
   * @param b The byte sequence containing the data.
   * @return A string representation of the contents of the provided byte
   *         sequence using hexadecimal characters.
   */
  public static String bytesToHex(ByteSequence b)
  {
    if (b == null || b.length() == 0)
    {
      return "";
    }

    int arrayLength = b.length();
    StringBuilder buffer = new StringBuilder((arrayLength - 1) * 3 + 2);
    buffer.append(byteToHex(b.byteAt(0)));

    for (int i=1; i < arrayLength; i++)
    {
      buffer.append(" ");
      buffer.append(byteToHex(b.byteAt(i)));
    }

    return buffer.toString();
  }



  /**
   * Retrieves a string representation of the contents of the provided byte
   * array using hexadecimal characters and a colon between each byte.
   *
   * @param  b  The byte array containing the data.
   *
   * @return  A string representation of the contents of the provided byte
   *          array using hexadecimal characters.
   */
  public static String bytesToColonDelimitedHex(byte[] b)
  {
    if (b == null || b.length == 0)
    {
      return "";
    }

    int arrayLength = b.length;
    StringBuilder buffer = new StringBuilder((arrayLength - 1) * 3 + 2);
    buffer.append(byteToHex(b[0]));

    for (int i=1; i < arrayLength; i++)
    {
      buffer.append(":");
      buffer.append(byteToHex(b[i]));
    }

    return buffer.toString();
  }



  /**
   * Retrieves a string representation of the contents of the provided byte
   * buffer using hexadecimal characters and a space between each byte.
   *
   * @param  b  The byte buffer containing the data.
   *
   * @return  A string representation of the contents of the provided byte
   *          buffer using hexadecimal characters.
   */
  public static String bytesToHex(ByteBuffer b)
  {
    if (b == null)
    {
      return "";
    }

    int position = b.position();
    int limit    = b.limit();
    int length   = limit - position;

    if (length == 0)
    {
      return "";
    }

    StringBuilder buffer = new StringBuilder((length - 1) * 3 + 2);
    buffer.append(byteToHex(b.get()));

    for (int i=1; i < length; i++)
    {
      buffer.append(" ");
      buffer.append(byteToHex(b.get()));
    }

    b.position(position);
    b.limit(limit);

    return buffer.toString();
  }



  /**
   * Appends a string representation of the provided byte array to the given
   * buffer using the specified indent.  The data will be formatted with sixteen
   * hex bytes in a row followed by the ASCII representation, then wrapping to a
   * new line as necessary.
   *
   * @param  buffer  The buffer to which the information is to be appended.
   * @param  b       The byte array containing the data to write.
   * @param  indent  The number of spaces to indent the output.
   */
  public static void byteArrayToHexPlusAscii(StringBuilder buffer, byte[] b,
                                             int indent)
  {
    StringBuilder indentBuf = new StringBuilder(indent);
    for (int i=0 ; i < indent; i++)
    {
      indentBuf.append(' ');
    }



    int length = b.length;
    int pos    = 0;
    while (length - pos >= 16)
    {
      StringBuilder asciiBuf = new StringBuilder(17);

      buffer.append(indentBuf);
      buffer.append(byteToHex(b[pos]));
      asciiBuf.append(byteToASCII(b[pos]));
      pos++;

      for (int i=1; i < 16; i++, pos++)
      {
        buffer.append(' ');
        buffer.append(byteToHex(b[pos]));
        asciiBuf.append(byteToASCII(b[pos]));

        if (i == 7)
        {
          buffer.append("  ");
          asciiBuf.append(' ');
        }
      }

      buffer.append("  ");
      buffer.append(asciiBuf);
      buffer.append(EOL);
    }


    int remaining = length - pos;
    if (remaining > 0)
    {
      StringBuilder asciiBuf = new StringBuilder(remaining+1);

      buffer.append(indentBuf);
      buffer.append(byteToHex(b[pos]));
      asciiBuf.append(byteToASCII(b[pos]));
      pos++;

      for (int i=1; i < 16; i++)
      {
        buffer.append(' ');

        if (i < remaining)
        {
          buffer.append(byteToHex(b[pos]));
          asciiBuf.append(byteToASCII(b[pos]));
          pos++;
        }
        else
        {
          buffer.append("  ");
        }

        if (i == 7)
        {
          buffer.append("  ");

          if (i < remaining)
          {
            asciiBuf.append(' ');
          }
        }
      }

      buffer.append("  ");
      buffer.append(asciiBuf);
      buffer.append(EOL);
    }
  }

  private static char byteToASCII(byte b)
  {
    return com.forgerock.opendj.util.StaticUtils.byteToASCII(b);
  }

  /**
   * Appends a string representation of the remaining unread data in the
   * provided byte buffer to the given buffer using the specified indent.
   * The data will be formatted with sixteen hex bytes in a row followed by
   * the ASCII representation, then wrapping to a new line as necessary.
   * The state of the byte buffer is not changed.
   *
   * @param  buffer  The buffer to which the information is to be appended.
   * @param  b       The byte buffer containing the data to write.
   *                 The data from the position to the limit is written.
   * @param  indent  The number of spaces to indent the output.
   */
  public static void byteArrayToHexPlusAscii(StringBuilder buffer, ByteBuffer b,
                                             int indent)
  {
    StringBuilder indentBuf = new StringBuilder(indent);
    for (int i=0 ; i < indent; i++)
    {
      indentBuf.append(' ');
    }


    int position = b.position();
    int limit    = b.limit();
    int length   = limit - position;
    int pos      = 0;
    while (length - pos >= 16)
    {
      StringBuilder asciiBuf = new StringBuilder(17);

      byte currentByte = b.get();
      buffer.append(indentBuf);
      buffer.append(byteToHex(currentByte));
      asciiBuf.append(byteToASCII(currentByte));
      pos++;

      for (int i=1; i < 16; i++, pos++)
      {
        currentByte = b.get();
        buffer.append(' ');
        buffer.append(byteToHex(currentByte));
        asciiBuf.append(byteToASCII(currentByte));

        if (i == 7)
        {
          buffer.append("  ");
          asciiBuf.append(' ');
        }
      }

      buffer.append("  ");
      buffer.append(asciiBuf);
      buffer.append(EOL);
    }


    int remaining = length - pos;
    if (remaining > 0)
    {
      StringBuilder asciiBuf = new StringBuilder(remaining+1);

      byte currentByte = b.get();
      buffer.append(indentBuf);
      buffer.append(byteToHex(currentByte));
      asciiBuf.append(byteToASCII(currentByte));

      for (int i=1; i < 16; i++)
      {
        buffer.append(' ');

        if (i < remaining)
        {
          currentByte = b.get();
          buffer.append(byteToHex(currentByte));
          asciiBuf.append(byteToASCII(currentByte));
        }
        else
        {
          buffer.append("  ");
        }

        if (i == 7)
        {
          buffer.append("  ");

          if (i < remaining)
          {
            asciiBuf.append(' ');
          }
        }
      }

      buffer.append("  ");
      buffer.append(asciiBuf);
      buffer.append(EOL);
    }

    b.position(position);
    b.limit(limit);
  }



  /**
   * Compare two byte arrays for order. Returns a negative integer,
   * zero, or a positive integer as the first argument is less than,
   * equal to, or greater than the second.
   *
   * @param a
   *          The first byte array to be compared.
   * @param a2
   *          The second byte array to be compared.
   * @return Returns a negative integer, zero, or a positive integer
   *         if the first byte array is less than, equal to, or greater
   *         than the second.
   */
  public static int compare(byte[] a, byte[] a2) {
    if (a == a2) {
      return 0;
    }

    if (a == null) {
      return -1;
    }

    if (a2 == null) {
      return 1;
    }

    int minLength = Math.min(a.length, a2.length);
    for (int i = 0; i < minLength; i++) {
      int firstByte = 0xFF & a[i];
      int secondByte = 0xFF & a2[i];
      if (firstByte != secondByte) {
        if (firstByte < secondByte) {
          return -1;
        } else if (firstByte > secondByte) {
          return 1;
        }
      }
    }

    return a.length - a2.length;
  }



  /**
   * Indicates whether the two array lists are equal. They will be
   * considered equal if they have the same number of elements, and
   * the corresponding elements between them are equal (in the same
   * order).
   *
   * @param list1
   *          The first list for which to make the determination.
   * @param list2
   *          The second list for which to make the determination.
   * @return {@code true} if the two array lists are equal, or
   *         {@code false} if they are not.
   */
  public static boolean listsAreEqual(List<?> list1, List<?> list2)
  {
    if (list1 == null)
    {
      return list2 == null;
    }
    else if (list2 == null)
    {
      return false;
    }

    int numElements = list1.size();
    if (numElements != list2.size())
    {
      return false;
    }

    // If either of the lists doesn't support random access, then fall back
    // on their equals methods and go ahead and create some garbage with the
    // iterators.
    if (!(list1 instanceof RandomAccess) ||
        !(list2 instanceof RandomAccess))
    {
      return list1.equals(list2);
    }

    // Otherwise we can just retrieve the elements efficiently via their index.
    for (int i=0; i < numElements; i++)
    {
      Object o1 = list1.get(i);
      Object o2 = list2.get(i);

      if (o1 == null)
      {
        if (o2 != null)
        {
          return false;
        }
      }
      else if (! o1.equals(o2))
      {
        return false;
      }
    }

    return true;
  }

  /**
   * Retrieves the best human-readable message for the provided exception.  For
   * exceptions defined in the OpenDJ project, it will attempt to use the
   * message (combining it with the message ID if available).  For some
   * exceptions that use encapsulation (e.g., InvocationTargetException), it
   * will be unwrapped and the cause will be treated.  For all others, the
   *
   *
   * @param  t  The {@code Throwable} object for which to retrieve the message.
   *
   * @return  The human-readable message generated for the provided exception.
   */
  public static LocalizableMessage getExceptionMessage(Throwable t)
  {
    if (t instanceof IdentifiedException)
    {
      IdentifiedException ie = (IdentifiedException) t;

      StringBuilder message = new StringBuilder();
      message.append(ie.getMessage());
      message.append(" (id=");
      LocalizableMessage ieMsg = ie.getMessageObject();
      if (ieMsg != null) {
        message.append(ieMsg.resourceName()).append("-").append(ieMsg.ordinal());
      } else {
        message.append("-1");
      }
      message.append(")");
      return LocalizableMessage.raw(message.toString());
    }
    else
    {
      return com.forgerock.opendj.util.StaticUtils.getExceptionMessage(t);
    }
  }



  /**
   * Retrieves a stack trace from the provided exception as a single-line
   * string.
   *
   * @param  t  The exception for which to retrieve the stack trace.
   *
   * @return  A stack trace from the provided exception as a single-line string.
   */
  public static String stackTraceToSingleLineString(Throwable t)
  {
    return com.forgerock.opendj.util.StaticUtils.stackTraceToSingleLineString(t, DynamicConstants.DEBUG_BUILD);
  }



  /**
   * Appends a single-line string representation of the provided exception to
   * the given buffer.
   *
   * @param  buffer  The buffer to which the information is to be appended.
   * @param  t       The exception for which to retrieve the stack trace.
   */
  public static void stackTraceToSingleLineString(StringBuilder buffer,
                                                  Throwable t)
  {
    com.forgerock.opendj.util.StaticUtils.stackTraceToSingleLineString(buffer, t, DynamicConstants.DEBUG_BUILD);
  }



  /**
   * Retrieves a string representation of the stack trace for the provided
   * exception.
   *
   * @param  t  The exception for which to retrieve the stack trace.
   *
   * @return  A string representation of the stack trace for the provided
   *          exception.
   */
  public static String stackTraceToString(Throwable t)
  {
    StringBuilder buffer = new StringBuilder();
    stackTraceToString(buffer, t);
    return buffer.toString();
  }

  /**
   * Check if the stack trace of provided exception contains a given cause.
   *
   * @param throwable
   *          exception that may contain the cause
   * @param searchedCause
   *          class of the cause to look for. Any subclass will match.
   * @return true if and only if the given cause is found as a cause of any
   *         level in the provided exception.
   */
  public static boolean stackTraceContainsCause(
      Throwable throwable, Class<? extends Throwable> searchedCause)
  {
    Throwable t = throwable;
    while ((t = t.getCause()) != null)
    {
      if (searchedCause.isAssignableFrom(t.getClass()))
      {
        return true;
      }

    }
    return false;
  }

  /**
   * Appends a string representation of the stack trace for the provided
   * exception to the given buffer.
   *
   * @param  buffer  The buffer to which the information is to be appended.
   * @param  t       The exception for which to retrieve the stack trace.
   */
  public static void stackTraceToString(StringBuilder buffer, Throwable t)
  {
    if (t == null)
    {
      return;
    }

    buffer.append(t);

    for (StackTraceElement e : t.getStackTrace())
    {
      buffer.append(EOL);
      buffer.append("  ");
      buffer.append(e.getClassName());
      buffer.append(".");
      buffer.append(e.getMethodName());
      buffer.append("(");
      buffer.append(e.getFileName());
      buffer.append(":");
      buffer.append(e.getLineNumber());
      buffer.append(")");
    }

    while (t.getCause() != null)
    {
      t = t.getCause();
      buffer.append(EOL);
      buffer.append("Caused by ");
      buffer.append(t);

      for (StackTraceElement e : t.getStackTrace())
      {
        buffer.append(EOL);
        buffer.append("  ");
        buffer.append(e.getClassName());
        buffer.append(".");
        buffer.append(e.getMethodName());
        buffer.append("(");
        buffer.append(e.getFileName());
        buffer.append(":");
        buffer.append(e.getLineNumber());
        buffer.append(")");
      }
    }

    buffer.append(EOL);
  }



  /**
   * Retrieves a backtrace for the current thread consisting only of filenames
   * and line numbers that may be useful in debugging the origin of problems
   * that should not have happened.  Note that this may be an expensive
   * operation to perform, so it should only be used for error conditions or
   * debugging.
   *
   * @return  A backtrace for the current thread.
   */
  public static String getBacktrace()
  {
    StringBuilder buffer = new StringBuilder();

    StackTraceElement[] elements = Thread.currentThread().getStackTrace();

    if (elements.length > 1)
    {
      buffer.append(elements[1].getFileName());
      buffer.append(":");
      buffer.append(elements[1].getLineNumber());

      for (int i=2; i < elements.length; i++)
      {
        buffer.append(" ");
        buffer.append(elements[i].getFileName());
        buffer.append(":");
        buffer.append(elements[i].getLineNumber());
      }
    }

    return buffer.toString();
  }



  /**
   * Retrieves a backtrace for the provided exception consisting of only
   * filenames and line numbers that may be useful in debugging the origin of
   * problems.  This is less expensive than the call to
   * {@code getBacktrace} without any arguments if an exception has already
   * been thrown.
   *
   * @param  t  The exception for which to obtain the backtrace.
   *
   * @return  A backtrace from the provided exception.
   */
  public static String getBacktrace(Throwable t)
  {
    StringBuilder buffer = new StringBuilder();

    StackTraceElement[] elements = t.getStackTrace();

    if (elements.length > 0)
    {
      buffer.append(elements[0].getFileName());
      buffer.append(":");
      buffer.append(elements[0].getLineNumber());

      for (int i=1; i < elements.length; i++)
      {
        buffer.append(" ");
        buffer.append(elements[i].getFileName());
        buffer.append(":");
        buffer.append(elements[i].getLineNumber());
      }
    }

    return buffer.toString();
  }



  /**
   * Indicates whether the provided character is a numeric digit.
   *
   * @param  c  The character for which to make the determination.
   *
   * @return  {@code true} if the provided character represents a numeric
   *          digit, or {@code false} if not.
   */
  public static boolean isDigit(final char c) {
    return com.forgerock.opendj.util.StaticUtils.isDigit(c);
  }



  /**
   * Indicates whether the provided character is an ASCII alphabetic character.
   *
   * @param  c  The character for which to make the determination.
   *
   * @return  {@code true} if the provided value is an uppercase or
   *          lowercase ASCII alphabetic character, or {@code false} if it
   *          is not.
   */
  public static boolean isAlpha(final char c) {
    return com.forgerock.opendj.util.StaticUtils.isAlpha(c);
  }

  /**
   * Indicates whether the provided character is a hexadecimal digit.
   *
   * @param  c  The character for which to make the determination.
   *
   * @return  {@code true} if the provided character represents a
   *          hexadecimal digit, or {@code false} if not.
   */
  public static boolean isHexDigit(final char c) {
    return com.forgerock.opendj.util.StaticUtils.isHexDigit(c);
  }

  /**
   * Indicates whether the provided byte represents a hexadecimal digit.
   *
   * @param  b  The byte for which to make the determination.
   *
   * @return  {@code true} if the provided byte represents a hexadecimal
   *          digit, or {@code false} if not.
   */
  public static boolean isHexDigit(byte b)
  {
    switch (b)
    {
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
      case 'A':
      case 'B':
      case 'C':
      case 'D':
      case 'E':
      case 'F':
      case 'a':
      case 'b':
      case 'c':
      case 'd':
      case 'e':
      case 'f':
        return true;
      default:
        return false;
    }
  }



  /**
   * Converts the provided hexadecimal string to a byte array.
   *
   * @param  hexString  The hexadecimal string to convert to a byte array.
   *
   * @return  The byte array containing the binary representation of the
   *          provided hex string.
   *
   * @throws  ParseException  If the provided string contains invalid
   *                          hexadecimal digits or does not contain an even
   *                          number of digits.
   */
  public static byte[] hexStringToByteArray(String hexString)
         throws ParseException
  {
    int length;
    if (hexString == null || ((length = hexString.length()) == 0))
    {
      return new byte[0];
    }


    if ((length % 2) == 1)
    {
      LocalizableMessage message = ERR_HEX_DECODE_INVALID_LENGTH.get(hexString);
      throw new ParseException(message.toString(), 0);
    }


    int pos = 0;
    int arrayLength = length / 2;
    byte[] returnArray = new byte[arrayLength];
    for (int i=0; i < arrayLength; i++)
    {
      switch (hexString.charAt(pos++))
      {
        case '0':
          returnArray[i] = 0x00;
          break;
        case '1':
          returnArray[i] = 0x10;
          break;
        case '2':
          returnArray[i] = 0x20;
          break;
        case '3':
          returnArray[i] = 0x30;
          break;
        case '4':
          returnArray[i] = 0x40;
          break;
        case '5':
          returnArray[i] = 0x50;
          break;
        case '6':
          returnArray[i] = 0x60;
          break;
        case '7':
          returnArray[i] = 0x70;
          break;
        case '8':
          returnArray[i] = (byte) 0x80;
          break;
        case '9':
          returnArray[i] = (byte) 0x90;
          break;
        case 'A':
        case 'a':
          returnArray[i] = (byte) 0xA0;
          break;
        case 'B':
        case 'b':
          returnArray[i] = (byte) 0xB0;
          break;
        case 'C':
        case 'c':
          returnArray[i] = (byte) 0xC0;
          break;
        case 'D':
        case 'd':
          returnArray[i] = (byte) 0xD0;
          break;
        case 'E':
        case 'e':
          returnArray[i] = (byte) 0xE0;
          break;
        case 'F':
        case 'f':
          returnArray[i] = (byte) 0xF0;
          break;
        default:
          LocalizableMessage message = ERR_HEX_DECODE_INVALID_CHARACTER.get(
              hexString, hexString.charAt(pos-1));
          throw new ParseException(message.toString(), 0);
      }

      switch (hexString.charAt(pos++))
      {
        case '0':
          // No action required.
          break;
        case '1':
          returnArray[i] |= 0x01;
          break;
        case '2':
          returnArray[i] |= 0x02;
          break;
        case '3':
          returnArray[i] |= 0x03;
          break;
        case '4':
          returnArray[i] |= 0x04;
          break;
        case '5':
          returnArray[i] |= 0x05;
          break;
        case '6':
          returnArray[i] |= 0x06;
          break;
        case '7':
          returnArray[i] |= 0x07;
          break;
        case '8':
          returnArray[i] |= 0x08;
          break;
        case '9':
          returnArray[i] |= 0x09;
          break;
        case 'A':
        case 'a':
          returnArray[i] |= 0x0A;
          break;
        case 'B':
        case 'b':
          returnArray[i] |= 0x0B;
          break;
        case 'C':
        case 'c':
          returnArray[i] |= 0x0C;
          break;
        case 'D':
        case 'd':
          returnArray[i] |= 0x0D;
          break;
        case 'E':
        case 'e':
          returnArray[i] |= 0x0E;
          break;
        case 'F':
        case 'f':
          returnArray[i] |= 0x0F;
          break;
        default:
          LocalizableMessage message = ERR_HEX_DECODE_INVALID_CHARACTER.get(
              hexString, hexString.charAt(pos-1));
          throw new ParseException(message.toString(), 0);
      }
    }

    return returnArray;
  }



  /**
   * Indicates whether the provided value needs to be base64-encoded if it is
   * represented in LDIF form.
   *
   * @param  valueBytes  The binary representation of the attribute value for
   *                     which to make the determination.
   *
   * @return  {@code true} if the value needs to be base64-encoded if it is
   *          represented in LDIF form, or {@code false} if not.
   */
  public static boolean needsBase64Encoding(ByteSequence valueBytes)
  {
    int length;
    if (valueBytes == null || ((length = valueBytes.length()) == 0))
    {
      return false;
    }


    // If the value starts with a space, colon, or less than, then it needs to
    // be base64-encoded.
    switch (valueBytes.byteAt(0))
    {
      case 0x20: // Space
      case 0x3A: // Colon
      case 0x3C: // Less-than
        return true;
    }


    // If the value ends with a space, then it needs to be base64-encoded.
    if (length > 1 && valueBytes.byteAt(length - 1) == 0x20)
    {
      return true;
    }


    // If the value contains a null, newline, or return character, then it needs
    // to be base64-encoded.
    byte b;
    for (int i = 0; i < valueBytes.length(); i++)
    {
      b = valueBytes.byteAt(i);
      if (b < 0 || 127 < b)
      {
        return true;
      }

      switch (b)
      {
        case 0x00: // Null
        case 0x0A: // New line
        case 0x0D: // Carriage return
          return true;
      }
    }


    // If we've made it here, then there's no reason to base64-encode.
    return false;
  }



  /**
   * Indicates whether the provided value needs to be base64-encoded if it is
   * represented in LDIF form.
   *
   * @param  valueString  The string representation of the attribute value for
   *                      which to make the determination.
   *
   * @return  {@code true} if the value needs to be base64-encoded if it is
   *          represented in LDIF form, or {@code false} if not.
   */
  public static boolean needsBase64Encoding(String valueString)
  {
    int length;
    if (valueString == null || ((length = valueString.length()) == 0))
    {
      return false;
    }


    // If the value starts with a space, colon, or less than, then it needs to
    // be base64-encoded.
    switch (valueString.charAt(0))
    {
      case ' ':
      case ':':
      case '<':
        return true;
    }


    // If the value ends with a space, then it needs to be base64-encoded.
    if (length > 1 && valueString.charAt(length - 1) == ' ')
    {
      return true;
    }


    // If the value contains a null, newline, or return character, then it needs
    // to be base64-encoded.
    for (int i=0; i < length; i++)
    {
      char c = valueString.charAt(i);
      if (c <= 0 || c == 0x0A || c == 0x0D || c > 127)
      {
        return true;
      }
    }


    // If we've made it here, then there's no reason to base64-encode.
    return false;
  }



  /**
   * Indicates whether the use of the exec method will be allowed on this
   * system.  It will be allowed by default, but that capability will be removed
   * if the org.opends.server.DisableExec system property is set and has any
   * value other than "false", "off", "no", or "0".
   *
   * @return  {@code true} if the use of the exec method should be allowed,
   *          or {@code false} if it should not be allowed.
   */
  public static boolean mayUseExec()
  {
    return !DirectoryServer.getEnvironmentConfig().disableExec();
  }



  /**
   * Executes the specified command on the system and captures its output.  This
   * will not return until the specified process has completed.
   *
   * @param  command           The command to execute.
   * @param  args              The set of arguments to provide to the command.
   * @param  workingDirectory  The working directory to use for the command, or
   *                           {@code null} if the default directory
   *                           should be used.
   * @param  environment       The set of environment variables that should be
   *                           set when executing the command, or
   *                           {@code null} if none are needed.
   * @param  output            The output generated by the command while it was
   *                           running.  This will include both standard
   *                           output and standard error.  It may be
   *                           {@code null} if the output does not need to
   *                           be captured.
   *
   * @return  The exit code for the command.
   *
   * @throws  IOException  If an I/O problem occurs while trying to execute the
   *                       command.
   *
   * @throws  SecurityException  If the security policy will not allow the
   *                             command to be executed.
   *
   * @throws InterruptedException If the current thread is interrupted by
   *                              another thread while it is waiting, then
   *                              the wait is ended and an InterruptedException
   *                              is thrown.
   */
  public static int exec(String command, String[] args, File workingDirectory,
                         Map<String,String> environment, List<String> output)
         throws IOException, SecurityException, InterruptedException
  {
    // See whether we'll allow the use of exec on this system.  If not, then
    // throw an exception.
    if (! mayUseExec())
    {
      throw new SecurityException(ERR_EXEC_DISABLED.get(command).toString());
    }


    ArrayList<String> commandAndArgs = new ArrayList<>();
    commandAndArgs.add(command);
    if (args != null && args.length > 0)
    {
      Collections.addAll(commandAndArgs, args);
    }

    ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs);
    processBuilder.redirectErrorStream(true);

    if (workingDirectory != null && workingDirectory.isDirectory())
    {
      processBuilder.directory(workingDirectory);
    }

    if (environment != null && !environment.isEmpty())
    {
      processBuilder.environment().putAll(environment);
    }

    Process process = processBuilder.start();

    // We must exhaust stdout and stderr before calling waitfor. Since we
    // redirected the error stream, we just have to read from stdout.
    InputStream processStream =  process.getInputStream();
    BufferedReader reader =
        new BufferedReader(new InputStreamReader(processStream));
    String line = null;

    try
    {
      while((line = reader.readLine()) != null)
      {
        if(output != null)
        {
          output.add(line);
        }
      }
    }
    catch(IOException ioe)
    {
      // If this happens, then we have no choice but to forcefully terminate
      // the process.
      try
      {
        process.destroy();
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }

      throw ioe;
    }
    finally
    {
      try
      {
        reader.close();
      }
      catch(IOException e)
      {
        logger.traceException(e);
      }
    }

    return process.waitFor();
  }



  /**
   * Indicates whether the provided string contains a name or OID for a schema
   * element like an attribute type or objectclass.
   *
   * @param  element        The string containing the substring for which to
   *                        make the determination.
   * @param  startPos       The position of the first character that is to be
   *                        checked.
   * @param  endPos         The position of the first character after the start
   *                        position that is not to be checked.
   * @param  invalidReason  The buffer to which the invalid reason is to be
   *                        appended if a problem is found.
   *
   * @return  {@code true} if the provided string contains a valid name or
   *          OID for a schema element, or {@code false} if it does not.
   */
  public static boolean isValidSchemaElement(String element, int startPos,
                                             int endPos,
                                             LocalizableMessageBuilder invalidReason)
  {
    if (element == null || startPos >= endPos)
    {
      invalidReason.append(ERR_SCHEMANAME_EMPTY_VALUE.get());
      return false;
    }


    char c = element.charAt(startPos);
    if (isAlpha(c))
    {
      // This can only be a name and not an OID.  The only remaining characters
      // must be letters, digits, dashes, and possibly the underscore.
      for (int i=startPos+1; i < endPos; i++)
      {
        c = element.charAt(i);
        if (!isAlpha(c)
            && !isDigit(c)
            && c != '-'
            && (c != '_' || !DirectoryServer.allowAttributeNameExceptions()))
        {
          // This is an illegal character for an attribute name.
          invalidReason.append(ERR_SCHEMANAME_ILLEGAL_CHAR.get(element, c, i));
          return false;
        }
      }
    }
    else if (isDigit(c))
    {
      // This should indicate an OID, but it may also be a name if name
      // exceptions are enabled.  Since we don't know for sure, we'll just
      // hold off until we know for sure.
      boolean isKnown    = !DirectoryServer.allowAttributeNameExceptions();
      boolean isNumeric  = true;
      boolean lastWasDot = false;

      for (int i=startPos+1; i < endPos; i++)
      {
        c = element.charAt(i);
        if (c == '.')
        {
          if (isKnown)
          {
            if (isNumeric)
            {
              // This is probably legal unless the last character was also a
              // period.
              if (lastWasDot)
              {
                invalidReason.append(ERR_SCHEMANAME_CONSECUTIVE_PERIODS.get(
                        element, i));
                return false;
              }
              else
              {
                lastWasDot = true;
              }
            }
            else
            {
              // This is an illegal character.
              invalidReason.append(ERR_SCHEMANAME_ILLEGAL_CHAR.get(
                      element, c, i));
              return false;
            }
          }
          else
          {
            // Now we know that this must be a numeric OID and not an attribute
            // name with exceptions allowed.
            lastWasDot = true;
            isKnown    = true;
            isNumeric  = true;
          }
        }
        else
        {
          lastWasDot = false;

          if (isAlpha(c) || c == '-' || c == '_')
          {
            if (isKnown)
            {
              if (isNumeric)
              {
                // This is an illegal character for a numeric OID.
                invalidReason.append(ERR_SCHEMANAME_ILLEGAL_CHAR.get(
                        element, c, i));
                return false;
              }
            }
            else
            {
              // Now we know that this must be an attribute name with exceptions
              // allowed and not a numeric OID.
              isKnown   = true;
              isNumeric = false;
            }
          }
          else if (! isDigit(c))
          {
            // This is an illegal character.
            invalidReason.append(ERR_SCHEMANAME_ILLEGAL_CHAR.get(
                    element, c, i));
            return false;
          }
        }
      }
    }
    else
    {
      // This is an illegal character.
      invalidReason.append(ERR_SCHEMANAME_ILLEGAL_CHAR.get(
              element, c, startPos));
      return false;
    }


    // If we've gotten here, then the value is fine.
    return true;
  }



  /**
   * Indicates whether the provided TCP address is already in use.
   *
   * @param  address        IP address of the TCP address for which to make
   *                        the determination.
   * @param  port           TCP port number of the TCP address for which to
   *                        make the determination.
   * @param  allowReuse     Whether or not TCP address reuse is allowed when
   *                        making the determination.
   *
   * @return  {@code true} if the provided TCP address is already in
   *          use, or {@code false} otherwise.
   */
  public static boolean isAddressInUse(
    InetAddress address, int port,
    boolean allowReuse)
  {
    // Return pessimistic.
    boolean isInUse = true;
    Socket clientSocket = null;
    ServerSocket serverSocket = null;
    try {
      // HACK:
      // With dual stacks we can have a situation when INADDR_ANY/PORT
      // is bound in TCP4 space but available in TCP6 space and since
      // JavaServerSocket implemantation will always use TCP46 on dual
      // stacks the bind below will always succeed in such cases thus
      // shadowing anything that is already bound to INADDR_ANY/PORT.
      // While technically correct, with IPv4 and IPv6 being separate
      // address spaces, it presents a problem to end users because a
      // common case scenario is to have a single service serving both
      // address spaces ie listening to the same port in both spaces
      // on wildcard addresses 0 and ::. ServerSocket implemantation
      // does not provide any means of working with each address space
      // separately such as doing TCP4 or TCP6 only binds thus we have
      // to do a dummy connect to INADDR_ANY/PORT to check if it is
      // bound to something already. This is only needed for wildcard
      // addresses as specific IPv4 or IPv6 addresses will always be
      // handled in their respective address space.
      if (address.isAnyLocalAddress()) {
        clientSocket = new Socket();
        try {
          // This might fail on some stacks but this is the best we
          // can do. No need for explicit timeout since it is local
          // address and we have to know for sure unless it fails.
          clientSocket.connect(new InetSocketAddress(address, port));
        } catch (IOException e) {
        // Expected, ignore.
        }
        if (clientSocket.isConnected()) {
          return true;
        }
      }
      serverSocket = new ServerSocket();
      serverSocket.setReuseAddress(allowReuse);
      serverSocket.bind(new InetSocketAddress(address, port));
      isInUse = false;
    } catch (IOException e) {
      isInUse = true;
    } finally {
      try {
        if (serverSocket != null) {
          serverSocket.close();
        }
      } catch (Exception e) {}
      try {
        if (clientSocket != null) {
          clientSocket.close();
        }
      } catch (Exception e) {}
    }
    return isInUse;
  }



  /**
   * Returns a lower-case string representation of a given string, verifying for null input string.
   * {@see com.forgerock.opendj.util.StaticUtils#toLowerCase(String s)}
   *
   * @param s the mixed case string
   * @return a lower-case string
   */
  public static String toLowerCase(String s)
  {
    return (s == null ? null : com.forgerock.opendj.util.StaticUtils.toLowerCase(s));
  }

  /**
   * Appends a lower-case string representation of a given ByteSequence to a StringBuilder,
   * verifying for null input.
   * {@see com.forgerock.opendj.util.StaticUtils#toLowerCase(ByteSequence s, StringBuilder string)}
   *
   * @param  b       The byte array for which to obtain the lowercase string
   *                 representation.
   * @param  buffer  The buffer to which the lowercase form of the string should
   *                 be appended.
   * @param  trim    Indicates whether leading and trailing spaces should be
   *                 omitted from the string representation.
   */
  public static void toLowerCase(ByteSequence b, StringBuilder buffer, boolean trim)
  {
    if (b == null)
    {
      return;
    }

    if (trim)
    {
      int begin = 0;
      int end = b.length() - 1;
      while (begin <= end)
      {
        if (b.byteAt(begin) == ' ')
        {
          begin++;
        }
        else if (b.byteAt(end) == ' ')
        {
          end--;
        }
        else
        {
          break;
        }
      }
      if (begin > 0 || end < b.length() - 1)
      {
        b = b.subSequence(begin, end + 1);
      }
    }

    com.forgerock.opendj.util.StaticUtils.toLowerCase(b, buffer);
  }



  /**
   * Retrieves an uppercase representation of the given string.  This
   * implementation presumes that the provided string will contain only ASCII
   * characters and is optimized for that case.  However, if a non-ASCII
   * character is encountered it will fall back on a more expensive algorithm
   * that will work properly for non-ASCII characters.
   *
   * @param  s  The string for which to obtain the uppercase representation.
   *
   * @return  The uppercase representation of the given string.
   */
  public static String toUpperCase(String s)
  {
    if (s == null)
    {
      return null;
    }

    StringBuilder buffer = new StringBuilder(s.length());
    toUpperCase(s, buffer);
    return buffer.toString();
  }



  /**
   * Appends an uppercase representation of the given string to the provided
   * buffer.  This implementation presumes that the provided string will contain
   * only ASCII characters and is optimized for that case.  However, if a
   * non-ASCII character is encountered it will fall back on a more expensive
   * algorithm that will work properly for non-ASCII characters.
   *
   * @param  s       The string for which to obtain the uppercase
   *                 representation.
   * @param  buffer  The buffer to which the uppercase form of the string should
   *                 be appended.
   */
  public static void toUpperCase(String s, StringBuilder buffer)
  {
    if (s == null)
    {
      return;
    }

    int length = s.length();
    for (int i=0; i < length; i++)
    {
      char c = s.charAt(i);

      if ((c & 0x7F) != c)
      {
        buffer.append(s.substring(i).toUpperCase());
        return;
      }

      switch (c)
      {
        case 'a':
          buffer.append('A');
          break;
        case 'b':
          buffer.append('B');
          break;
        case 'c':
          buffer.append('C');
          break;
        case 'd':
          buffer.append('D');
          break;
        case 'e':
          buffer.append('E');
          break;
        case 'f':
          buffer.append('F');
          break;
        case 'g':
          buffer.append('G');
          break;
        case 'h':
          buffer.append('H');
          break;
        case 'i':
          buffer.append('I');
          break;
        case 'j':
          buffer.append('J');
          break;
        case 'k':
          buffer.append('K');
          break;
        case 'l':
          buffer.append('L');
          break;
        case 'm':
          buffer.append('M');
          break;
        case 'n':
          buffer.append('N');
          break;
        case 'o':
          buffer.append('O');
          break;
        case 'p':
          buffer.append('P');
          break;
        case 'q':
          buffer.append('Q');
          break;
        case 'r':
          buffer.append('R');
          break;
        case 's':
          buffer.append('S');
          break;
        case 't':
          buffer.append('T');
          break;
        case 'u':
          buffer.append('U');
          break;
        case 'v':
          buffer.append('V');
          break;
        case 'w':
          buffer.append('W');
          break;
        case 'x':
          buffer.append('X');
          break;
        case 'y':
          buffer.append('Y');
          break;
        case 'z':
          buffer.append('Z');
          break;
        default:
          buffer.append(c);
      }
    }
  }



  /**
   * Appends an uppercase string representation of the contents of the given
   * byte array to the provided buffer, optionally trimming leading and trailing
   * spaces.  This implementation presumes that the provided string will contain
   * only ASCII characters and is optimized for that case.  However, if a
   * non-ASCII character is encountered it will fall back on a more expensive
   * algorithm that will work properly for non-ASCII characters.
   *
   * @param  b       The byte array for which to obtain the uppercase string
   *                 representation.
   * @param  buffer  The buffer to which the uppercase form of the string should
   *                 be appended.
   * @param  trim    Indicates whether leading and trailing spaces should be
   *                 omitted from the string representation.
   */
  public static void toUpperCase(byte[] b, StringBuilder buffer, boolean trim)
  {
    if (b == null)
    {
      return;
    }

    int length = b.length;
    for (int i=0; i < length; i++)
    {
      if ((b[i] & 0x7F) != b[i])
      {
        try
        {
          buffer.append(new String(b, i, (length-i), "UTF-8").toUpperCase());
        }
        catch (Exception e)
        {
          logger.traceException(e);
          buffer.append(new String(b, i, (length - i)).toUpperCase());
        }
        break;
      }

      int bufferLength = buffer.length();
      switch (b[i])
      {
        case ' ':
          // If we don't care about trimming, then we can always append the
          // space.  Otherwise, only do so if there are other characters in the value.
          if (trim && bufferLength == 0)
          {
            break;
          }

          buffer.append(' ');
          break;
        case 'a':
          buffer.append('A');
          break;
        case 'b':
          buffer.append('B');
          break;
        case 'c':
          buffer.append('C');
          break;
        case 'd':
          buffer.append('D');
          break;
        case 'e':
          buffer.append('E');
          break;
        case 'f':
          buffer.append('F');
          break;
        case 'g':
          buffer.append('G');
          break;
        case 'h':
          buffer.append('H');
          break;
        case 'i':
          buffer.append('I');
          break;
        case 'j':
          buffer.append('J');
          break;
        case 'k':
          buffer.append('K');
          break;
        case 'l':
          buffer.append('L');
          break;
        case 'm':
          buffer.append('M');
          break;
        case 'n':
          buffer.append('N');
          break;
        case 'o':
          buffer.append('O');
          break;
        case 'p':
          buffer.append('P');
          break;
        case 'q':
          buffer.append('Q');
          break;
        case 'r':
          buffer.append('R');
          break;
        case 's':
          buffer.append('S');
          break;
        case 't':
          buffer.append('T');
          break;
        case 'u':
          buffer.append('U');
          break;
        case 'v':
          buffer.append('V');
          break;
        case 'w':
          buffer.append('W');
          break;
        case 'x':
          buffer.append('X');
          break;
        case 'y':
          buffer.append('Y');
          break;
        case 'z':
          buffer.append('Z');
          break;
        default:
          buffer.append((char) b[i]);
      }
    }

    if (trim)
    {
      // Strip off any trailing spaces.
      for (int i=buffer.length()-1; i > 0; i--)
      {
        if (buffer.charAt(i) == ' ')
        {
          buffer.delete(i, i+1);
        }
        else
        {
          break;
        }
      }
    }
  }



  /**
   * Append a string to a string builder, escaping any double quotes
   * according to the StringValue production in RFC 3641.
   * <p>
   * In RFC 3641 the StringValue production looks like this:
   *
   * <pre>
   *    StringValue       = dquote *SafeUTF8Character dquote
   *    dquote            = %x22 ; &quot; (double quote)
   *    SafeUTF8Character = %x00-21 / %x23-7F /   ; ASCII minus dquote
   *                        dquote dquote /       ; escaped double quote
   *                        %xC0-DF %x80-BF /     ; 2 byte UTF-8 character
   *                        %xE0-EF 2(%x80-BF) /  ; 3 byte UTF-8 character
   *                        %xF0-F7 3(%x80-BF)    ; 4 byte UTF-8 character
   * </pre>
   *
   * <p>
   * That is, strings are surrounded by double-quotes and any internal
   * double-quotes are doubled up.
   *
   * @param builder
   *          The string builder.
   * @param string
   *          The string to escape and append.
   * @return Returns the string builder.
   */
  public static StringBuilder toRFC3641StringValue(StringBuilder builder,
      String string)
  {
    // Initial double-quote.
    builder.append('"');

    for (char c : string.toCharArray())
    {
      if (c == '"')
      {
        // Internal double-quotes are escaped using a double-quote.
        builder.append('"');
      }
      builder.append(c);
    }

    // Trailing double-quote.
    builder.append('"');

    return builder;
  }



  /**
   * Retrieves a string array containing the contents of the provided
   * list of strings.
   *
   * @param stringList
   *          The string list to convert to an array.
   * @return A string array containing the contents of the provided list
   *         of strings.
   */
  public static String[] listToArray(List<String> stringList)
  {
    if (stringList == null)
    {
      return null;
    }

    String[] stringArray = new String[stringList.size()];
    stringList.toArray(stringArray);
    return stringArray;
  }

  /**
   * Retrieves an array list containing the contents of the provided array.
   *
   * @param  stringArray  The string array to convert to an array list.
   *
   * @return  An array list containing the contents of the provided array.
   */
  public static ArrayList<String> arrayToList(String... stringArray)
  {
    if (stringArray == null)
    {
      return null;
    }

    ArrayList<String> stringList = new ArrayList<>(stringArray.length);
    Collections.addAll(stringList, stringArray);
    return stringList;
  }


  /**
   * Attempts to delete the specified file or directory. If it is a directory,
   * then any files or subdirectories that it contains will be recursively
   * deleted as well.
   *
   * @param file
   *          The file or directory to be removed.
   * @return {@code true} if the specified file and any subordinates are all
   *         successfully removed, or {@code false} if at least one element in
   *         the subtree could not be removed or file does not exists.
   */
  public static boolean recursiveDelete(File file)
  {
    if (file.exists())
    {
      boolean successful = true;
      if (file.isDirectory())
      {
        File[] childList = file.listFiles();
        if (childList != null)
        {
          for (File f : childList)
          {
            successful &= recursiveDelete(f);
          }
        }
      }

      return successful & file.delete();
    }
    return false;
  }



  /**
   * Moves the indicated file to the specified directory by creating a new file
   * in the target directory, copying the contents of the existing file, and
   * removing the existing file.  The file to move must exist and must be a
   * file.  The target directory must exist, must be a directory, and must not
   * be the directory in which the file currently resides.
   *
   * @param  fileToMove       The file to move to the target directory.
   * @param  targetDirectory  The directory into which the file should be moved.
   *
   * @throws  IOException  If a problem occurs while attempting to move the
   *                       file.
   */
  public static void moveFile(File fileToMove, File targetDirectory)
         throws IOException
  {
    if (! fileToMove.exists())
    {
      LocalizableMessage message = ERR_MOVEFILE_NO_SUCH_FILE.get(fileToMove.getPath());
      throw new IOException(message.toString());
    }

    if (! fileToMove.isFile())
    {
      LocalizableMessage message = ERR_MOVEFILE_NOT_FILE.get(fileToMove.getPath());
      throw new IOException(message.toString());
    }

    if (! targetDirectory.exists())
    {
      LocalizableMessage message =
          ERR_MOVEFILE_NO_SUCH_DIRECTORY.get(targetDirectory.getPath());
      throw new IOException(message.toString());
    }

    if (! targetDirectory.isDirectory())
    {
      LocalizableMessage message =
          ERR_MOVEFILE_NOT_DIRECTORY.get(targetDirectory.getPath());
      throw new IOException(message.toString());
    }

    String newFilePath = targetDirectory.getPath() + File.separator +
                         fileToMove.getName();
    FileInputStream  inputStream  = new FileInputStream(fileToMove);
    FileOutputStream outputStream = new FileOutputStream(newFilePath, false);
    byte[] buffer = new byte[8192];
    while (true)
    {
      int bytesRead = inputStream.read(buffer);
      if (bytesRead < 0)
      {
        break;
      }

      outputStream.write(buffer, 0, bytesRead);
    }

    outputStream.flush();
    outputStream.close();
    inputStream.close();
    fileToMove.delete();
  }

  /**
   * Renames the source file to the target file.  If the target file exists
   * it is first deleted.  The rename and delete operation return values
   * are checked for success and if unsuccessful, this method throws an
   * exception.
   *
   * @param fileToRename The file to rename.
   * @param target       The file to which {@code fileToRename} will be
   *                     moved.
   * @throws IOException If a problem occurs while attempting to rename the
   *                     file.  On the Windows platform, this typically
   *                     indicates that the file is in use by this or another
   *                     application.
   */
  public static void renameFile(File fileToRename, File target)
          throws IOException {
    if (fileToRename != null && target != null)
    {
      synchronized(target)
      {
        if (target.exists() && !target.delete())
        {
          LocalizableMessage message =
              ERR_RENAMEFILE_CANNOT_DELETE_TARGET.get(target.getPath());
          throw new IOException(message.toString());
        }
      }
      if (!fileToRename.renameTo(target))
      {
        LocalizableMessage message = ERR_RENAMEFILE_CANNOT_RENAME.get(
            fileToRename.getPath(), target.getPath());
        throw new IOException(message.toString());

      }
    }
  }


  /**
   * Indicates whether the provided path refers to a relative path rather than
   * an absolute path.
   *
   * @param  path  The path string for which to make the determination.
   *
   * @return  {@code true} if the provided path is relative, or
   *          {@code false} if it is absolute.
   */
  public static boolean isRelativePath(String path)
  {
    File f = new File(path);
    return !f.isAbsolute();
  }



  /**
   * Retrieves a {@code File} object corresponding to the specified path.
   * If the given path is an absolute path, then it will be used.  If the path
   * is relative, then it will be interpreted as if it were relative to the
   * Directory Server root.
   *
   * @param  path  The path string to be retrieved as a {@code File}
   *
   * @return  A {@code File} object that corresponds to the specified path.
   */
  public static File getFileForPath(String path)
  {
    File f = new File (path);

    if (f.isAbsolute())
    {
      return f;
    }
    else
    {
      return new File(DirectoryServer.getInstanceRoot() + File.separator +
          path);
    }
  }

  /**
   * Retrieves a {@code File} object corresponding to the specified path.
   * If the given path is an absolute path, then it will be used.  If the path
   * is relative, then it will be interpreted as if it were relative to the
   * Directory Server root.
   *
   * @param path
   *           The path string to be retrieved as a {@code File}.
   * @param serverContext
   *           The server context.
   *
   * @return  A {@code File} object that corresponds to the specified path.
   */
  public static File getFileForPath(String path, ServerContext serverContext)
  {
    File f = new File (path);

    if (f.isAbsolute())
    {
      return f;
    }
    else
    {
      return new File(serverContext.getInstanceRoot() + File.separator +
          path);
    }
  }



  /**
   * Creates a new, blank entry with the given DN.  It will contain only the
   * attribute(s) contained in the RDN.  The choice of objectclasses will be
   * based on the RDN attribute.  If there is a single RDN attribute, then the
   * following mapping will be used:
   * <BR>
   * <UL>
   *   <LI>c attribute :: country objectclass</LI>
   *   <LI>dc attribute :: domain objectclass</LI>
   *   <LI>o attribute :: organization objectclass</LI>
   *   <LI>ou attribute :: organizationalUnit objectclass</LI>
   * </UL>
   * <BR>
   * Any other single RDN attribute types, or any case in which there are
   * multiple RDN attributes, will use the untypedObject objectclass.  If the
   * RDN includes one or more attributes that are not allowed in the
   * untypedObject objectclass, then the extensibleObject class will also be
   * added.  Note that this method cannot be used to generate an entry
   * with an empty or null DN.
   *
   * @param  dn  The DN to use for the entry.
   *
   * @return  The entry created with the provided DN.
   */
  public static Entry createEntry(DN dn)
  {
    // If the provided DN was null or empty, then return null because we don't
    // support it.
    if (dn == null || dn.isRootDN())
    {
      return null;
    }


    // Get the information about the RDN attributes.
    RDN rdn = dn.rdn();
    int numAVAs = rdn.getNumValues();

    // If there is only one RDN attribute, then see which objectclass we should use.
    ObjectClass structuralClass = DirectoryServer.getObjectClass(getObjectClassName(rdn, numAVAs));

    // Get the top and untypedObject classes to include in the entry.
    LinkedHashMap<ObjectClass,String> objectClasses = new LinkedHashMap<>(3);

    objectClasses.put(DirectoryServer.getTopObjectClass(), OC_TOP);
    objectClasses.put(structuralClass, structuralClass.getNameOrOID());


    // Iterate through the RDN attributes and add them to the set of user or
    // operational attributes.
    LinkedHashMap<AttributeType,List<Attribute>> userAttributes = new LinkedHashMap<>();
    LinkedHashMap<AttributeType,List<Attribute>> operationalAttributes = new LinkedHashMap<>();

    boolean extensibleObjectAdded = false;
    for (int i=0; i < numAVAs; i++)
    {
      AttributeType attrType = rdn.getAttributeType(i);
      ByteString attrValue = rdn.getAttributeValue(i);
      String attrName = rdn.getAttributeName(i);

      // First, see if this type is allowed by the untypedObject class.  If not,
      // then we'll need to include the extensibleObject class.
      if (!structuralClass.isRequiredOrOptional(attrType) && !extensibleObjectAdded)
      {
        ObjectClass extensibleObjectOC =
             DirectoryServer.getObjectClass(OC_EXTENSIBLE_OBJECT_LC);
        if (extensibleObjectOC == null)
        {
          extensibleObjectOC =
               DirectoryServer.getDefaultObjectClass(OC_EXTENSIBLE_OBJECT);
        }
        objectClasses.put(extensibleObjectOC, OC_EXTENSIBLE_OBJECT);
        extensibleObjectAdded = true;
      }


      // Create the attribute and add it to the appropriate map.
      if (attrType.isOperational())
      {
        addAttributeValue(operationalAttributes, attrType, attrName, attrValue);
      }
      else
      {
        addAttributeValue(userAttributes, attrType, attrName, attrValue);
      }
    }


    // Create and return the entry.
    return new Entry(dn, objectClasses, userAttributes, operationalAttributes);
  }

  private static String getObjectClassName(RDN rdn, int numAVAs)
  {
    if (numAVAs == 1)
    {
      final AttributeType attrType = rdn.getAttributeType(0);
      if (attrType.hasName(ATTR_C))
      {
        return OC_COUNTRY;
      }
      else if (attrType.hasName(ATTR_DC))
      {
        return OC_DOMAIN;
      }
      else if (attrType.hasName(ATTR_O))
      {
        return OC_ORGANIZATION;
      }
      else if (attrType.hasName(ATTR_OU))
      {
        return OC_ORGANIZATIONAL_UNIT_LC;
      }
    }
    return OC_UNTYPED_OBJECT_LC;
  }

  private static void addAttributeValue(LinkedHashMap<AttributeType, List<Attribute>> attrs,
      AttributeType attrType, String attrName, ByteString attrValue)
  {
    List<Attribute> attrList = attrs.get(attrType);
    if (attrList != null && !attrList.isEmpty())
    {
      AttributeBuilder builder = new AttributeBuilder(attrList.get(0));
      builder.add(attrValue);
      attrList.set(0, builder.toAttribute());
    }
    else
    {
      AttributeBuilder builder = new AttributeBuilder(attrType, attrName);
      builder.add(attrValue);
      attrs.put(attrType, builder.toAttributeList());
    }
  }

  /**
   * Retrieves a user-friendly string that indicates the length of time (in
   * days, hours, minutes, and seconds) in the specified number of seconds.
   *
   * @param  numSeconds  The number of seconds to be converted to a more
   *                     user-friendly value.
   *
   * @return  The user-friendly representation of the specified number of
   *          seconds.
   */
  public static LocalizableMessage secondsToTimeString(long numSeconds)
  {
    if (numSeconds < 60)
    {
      // We can express it in seconds.
      return INFO_TIME_IN_SECONDS.get(numSeconds);
    }
    else if (numSeconds < 3600)
    {
      // We can express it in minutes and seconds.
      long m = numSeconds / 60;
      long s = numSeconds % 60;
      return INFO_TIME_IN_MINUTES_SECONDS.get(m, s);
    }
    else if (numSeconds < 86400)
    {
      // We can express it in hours, minutes, and seconds.
      long h = numSeconds / 3600;
      long m = (numSeconds % 3600) / 60;
      long s = numSeconds % 3600 % 60;
      return INFO_TIME_IN_HOURS_MINUTES_SECONDS.get(h, m, s);
    }
    else
    {
      // We can express it in days, hours, minutes, and seconds.
      long d = numSeconds / 86400;
      long h = (numSeconds % 86400) / 3600;
      long m = (numSeconds % 86400 % 3600) / 60;
      long s = numSeconds % 86400 % 3600 % 60;
      return INFO_TIME_IN_DAYS_HOURS_MINUTES_SECONDS.get(d, h, m, s);
    }
  }

  /**
   * Checks that no more that one of a set of arguments is present.  This
   * utility should be used after argument parser has parsed a set of
   * arguments.
   *
   * @param  args to test for the presence of more than one
   * @throws ArgumentException if more than one of {@code args} is
   *         present and containing an error message identifying the
   *         arguments in violation
   */
  public static void checkOnlyOneArgPresent(Argument... args)
    throws ArgumentException
  {
    if (args != null) {
      for (Argument arg : args) {
        for (Argument otherArg : args) {
          if (arg != otherArg && arg.isPresent() && otherArg.isPresent()) {
            throw new ArgumentException(
                    ToolMessages.ERR_INCOMPATIBLE_ARGUMENTS.get(
                            arg.getName(), otherArg.getName()));
          }
        }
      }
    }
  }

  /**
   * Converts a string representing a time in "yyyyMMddHHmmss.SSS'Z'" or
   * "yyyyMMddHHmmss" to a {@code Date}.
   *
   * @param timeStr string formatted appropriately
   * @return Date object; null if {@code timeStr} is null
   * @throws ParseException if there was a problem converting the string to
   *         a {@code Date}.
   */
  public static Date parseDateTimeString(String timeStr) throws ParseException
  {
    Date dateTime = null;
    if (timeStr != null)
    {
      if (timeStr.endsWith("Z"))
      {
        try
        {
          SimpleDateFormat dateFormat =
            new SimpleDateFormat(DATE_FORMAT_GENERALIZED_TIME);
          dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
          dateFormat.setLenient(true);
          dateTime = dateFormat.parse(timeStr);
        }
        catch (ParseException pe)
        {
          // Best effort: try with GMT time.
          SimpleDateFormat dateFormat =
            new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
          dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
          dateFormat.setLenient(true);
          dateTime = dateFormat.parse(timeStr);
        }
      }
      else
      {
        SimpleDateFormat dateFormat =
            new SimpleDateFormat(DATE_FORMAT_COMPACT_LOCAL_TIME);
        dateFormat.setLenient(true);
        dateTime = dateFormat.parse(timeStr);
      }
    }
    return dateTime;
  }

  /**
   * Formats a Date to String representation in "yyyyMMddHHmmss'Z'".
   *
   * @param date to format; null if {@code date} is null
   * @return string representation of the date
   */
  public static String formatDateTimeString(Date date)
  {
    String timeStr = null;
    if (date != null)
    {
      SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
      dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
      timeStr = dateFormat.format(date);
    }
    return timeStr;
  }

  /**
   * Indicates whether or not a string represents a syntactically correct
   * email address.
   *
   * @param addr to validate
   * @return boolean where {@code true} indicates that the string is a
   *         syntactically correct email address
   */
  public static boolean isEmailAddress(String addr) {

    // This just does basic syntax checking.  Perhaps we
    // might want to be stricter about this.
    return addr != null && addr.contains("@") && addr.contains(".");

  }



  /**
   * Writes the contents of the provided buffer to the client,
   * terminating the connection if the write is unsuccessful for too
   * long (e.g., if the client is unresponsive or there is a network
   * problem). If possible, it will attempt to use the selector returned
   * by the {@code ClientConnection.getWriteSelector} method, but it is
   * capable of working even if that method returns {@code null}. <BR>
   *
   * Note that the original position and limit values will not be
   * preserved, so if that is important to the caller, then it should
   * record them before calling this method and restore them after it
   * returns.
   *
   * @param clientConnection
   *          The client connection to which the data is to be written.
   * @param buffer
   *          The data to be written to the client.
   * @return {@code true} if all the data in the provided buffer was
   *         written to the client and the connection may remain
   *         established, or {@code false} if a problem occurred
   *         and the client connection is no longer valid. Note that if
   *         this method does return {@code false}, then it must
   *         have already disconnected the client.
   * @throws IOException
   *           If a problem occurs while attempting to write data to the
   *           client. The caller will be responsible for catching this
   *           and terminating the client connection.
   */
  public static boolean writeWithTimeout(ClientConnection clientConnection,
      ByteBuffer buffer) throws IOException
  {
    SocketChannel socketChannel = clientConnection.getSocketChannel();
    long startTime = System.currentTimeMillis();
    long waitTime = clientConnection.getMaxBlockedWriteTimeLimit();
    if (waitTime <= 0)
    {
      // We won't support an infinite time limit, so fall back to using
      // five minutes, which is a very long timeout given that we're
      // blocking a worker thread.
      waitTime = 300000L;
    }

    long stopTime = startTime + waitTime;

    Selector selector = clientConnection.getWriteSelector();
    if (selector == null)
    {
      // The client connection does not provide a selector, so we'll
      // fall back
      // to a more inefficient way that will work without a selector.
      while (buffer.hasRemaining()
          && System.currentTimeMillis() < stopTime)
      {
        if (socketChannel.write(buffer) < 0)
        {
          // The client connection has been closed.
          return false;
        }
      }

      if (buffer.hasRemaining())
      {
        // If we've gotten here, then the write timed out.
        return false;
      }

      return true;
    }

    // Register with the selector for handling write operations.
    SelectionKey key =
        socketChannel.register(selector, SelectionKey.OP_WRITE);

    try
    {
      selector.select(waitTime);
      while (buffer.hasRemaining())
      {
        long currentTime = System.currentTimeMillis();
        if (currentTime >= stopTime)
        {
          // We've been blocked for too long.
          return false;
        }
        else
        {
          waitTime = stopTime - currentTime;
        }

        Iterator<SelectionKey> iterator =
            selector.selectedKeys().iterator();
        while (iterator.hasNext())
        {
          SelectionKey k = iterator.next();
          if (k.isWritable())
          {
            int bytesWritten = socketChannel.write(buffer);
            if (bytesWritten < 0)
            {
              // The client connection has been closed.
              return false;
            }

            iterator.remove();
          }
        }

        if (buffer.hasRemaining())
        {
          selector.select(waitTime);
        }
      }

      return true;
    }
    finally
    {
      if (key.isValid())
      {
        key.cancel();
        selector.selectNow();
      }
    }
  }



  /**
   * Add all of the superior objectclasses to the specified objectclass
   * map if they don't already exist. Used by add and import-ldif to
   * add missing superior objectclasses to entries that don't have them.
   *
   * @param objectClasses A Map of objectclasses.
   */
  public static void addSuperiorObjectClasses(Map<ObjectClass,
      String> objectClasses) {
      HashSet<ObjectClass> additionalClasses = null;
      for (ObjectClass oc : objectClasses.keySet())
      {
        for(ObjectClass superiorClass : oc.getSuperiorClasses())
        {
          if (! objectClasses.containsKey(superiorClass))
          {
            if (additionalClasses == null)
            {
              additionalClasses = new HashSet<>();
            }

            additionalClasses.add(superiorClass);
          }
        }
      }

      if (additionalClasses != null)
      {
        for (ObjectClass oc : additionalClasses)
        {
          addObjectClassChain(oc, objectClasses);
        }
      }
  }

  private static void addObjectClassChain(ObjectClass objectClass,
      Map<ObjectClass, String> objectClasses)
  {
    if (objectClasses != null){
      if (! objectClasses.containsKey(objectClass))
      {
        objectClasses.put(objectClass, objectClass.getNameOrOID());
      }

      for(ObjectClass superiorClass : objectClass.getSuperiorClasses())
      {
        if (! objectClasses.containsKey(superiorClass))
        {
          addObjectClassChain(superiorClass, objectClasses);
        }
      }
    }
  }


  /**
   * Closes the provided {@link Closeable}'s ignoring any errors which
   * occurred.
   *
   * @param closeables The closeables to be closed, which may be
   *        {@code null}.
   */
  public static void close(Closeable... closeables)
  {
    if (closeables == null)
    {
      return;
    }
    close(Arrays.asList(closeables));
  }

  /**
   * Closes the provided {@link Closeable}'s ignoring any errors which occurred.
   *
   * @param closeables
   *          The closeables to be closed, which may be {@code null}.
   */
  public static void close(Collection<? extends Closeable> closeables)
  {
    if (closeables == null)
    {
      return;
    }
    for (Closeable closeable : closeables)
    {
      if (closeable != null)
      {
        try
        {
          closeable.close();
        }
        catch (IOException ignored)
        {
          logger.traceException(ignored);
        }
      }
    }
  }

  /**
   * Closes the provided {@link InitialContext}'s ignoring any errors which occurred.
   *
   * @param ctxs
   *          The contexts to be closed, which may be {@code null}.
   */
  public static void close(InitialContext... ctxs)
  {
    if (ctxs == null)
    {
      return;
    }
    for (InitialContext ctx : ctxs)
    {
      if (ctx != null)
      {
        try
        {
          ctx.close();
        }
        catch (NamingException ignored)
        {
          // ignore
        }
      }
    }
  }

  /**
   * Calls {@link Thread#sleep(long)}, surrounding it with the mandatory
   * {@code try} / {@code catch(InterruptedException)} block.
   *
   * @param millis
   *          the length of time to sleep in milliseconds
   */
  public static void sleep(long millis)
  {
    try
    {
      Thread.sleep(millis);
    }
    catch (InterruptedException wokenUp)
    {
      // ignore
    }
  }

  /**
   * Test if the provided message corresponds to the provided descriptor.
   *
   * @param msg
   *          The i18n message.
   * @param desc
   *          The message descriptor.
   * @return {@code true} if message corresponds to descriptor
   */
  public static boolean hasDescriptor(LocalizableMessage msg,
      LocalizableMessageDescriptor.Arg0 desc)
  {
    return msg.ordinal() == desc.ordinal()
        && msg.resourceName().equals(desc.resourceName());
  }

  /**
   * Test if the provided message corresponds to the provided descriptor.
   *
   * @param msg
   *          The i18n message.
   * @param desc
   *          The message descriptor.
   * @return {@code true} if message corresponds to descriptor
   */
  public static boolean hasDescriptor(LocalizableMessage msg,
      LocalizableMessageDescriptor.Arg1 desc)
  {
    return msg.ordinal() == desc.ordinal()
        && msg.resourceName().equals(desc.resourceName());
  }

  /**
   * Test if the provided message corresponds to the provided descriptor.
   *
   * @param msg
   *          The i18n message.
   * @param desc
   *          The message descriptor.
   * @return {@code true} if message corresponds to descriptor
   */
  public static boolean hasDescriptor(LocalizableMessage msg,
      LocalizableMessageDescriptor.Arg2 desc)
  {
    return msg.ordinal() == desc.ordinal()
        && msg.resourceName().equals(desc.resourceName());
  }

  /**
   * Test if the provided message corresponds to the provided descriptor.
   *
   * @param msg
   *          The i18n message.
   * @param desc
   *          The message descriptor.
   * @return {@code true} if message corresponds to descriptor
   */
  public static boolean hasDescriptor(LocalizableMessage msg,
      LocalizableMessageDescriptor.Arg3 desc)
  {
    return msg.ordinal() == desc.ordinal()
        && msg.resourceName().equals(desc.resourceName());
  }

  /**
   * Test if the provided message corresponds to the provided descriptor.
   *
   * @param msg
   *          The i18n message.
   * @param desc
   *          The message descriptor.
   * @return {@code true} if message corresponds to descriptor
   */
  public static boolean hasDescriptor(LocalizableMessage msg,
      LocalizableMessageDescriptor.Arg7 desc)
  {
    return msg.ordinal() == desc.ordinal()
        && msg.resourceName().equals(desc.resourceName());
  }

  /**
   * Returns an {@link Iterable} returning the passed in {@link Iterator}. THis
   * allows using methods returning Iterators with foreach statements.
   * <p>
   * For example, consider a method with this signature:
   * <p>
   * <code>public Iterator&lt;String&gt; myIteratorMethod();</code>
   * <p>
   * Classical use with for or while loop:
   *
   * <pre>
   * for (Iterator&lt;String&gt; it = myIteratorMethod(); it.hasNext();)
   * {
   *   String s = it.next();
   *   // use it
   * }
   *
   * Iterator&lt;String&gt; it = myIteratorMethod();
   * while(it.hasNext();)
   * {
   *   String s = it.next();
   *   // use it
   * }
   * </pre>
   *
   * Improved use with foreach:
   *
   * <pre>
   * for (String s : StaticUtils.toIterable(myIteratorMethod()))
   * {
   * }
   * </pre>
   *
   * </p>
   *
   * @param <T>
   *          the generic type of the passed in Iterator and for the returned
   *          Iterable.
   * @param iterator
   *          the Iterator that will be returned by the Iterable.
   * @return an Iterable returning the passed in Iterator
   */
  public static <T> Iterable<T> toIterable(final Iterator<T> iterator)
  {
    return new Iterable<T>()
    {
      @Override
      public Iterator<T> iterator()
      {
        return iterator;
      }
    };
  }

  /**
   * Returns true if the version of the server is an OEM one, and therefore doesn't support the JE backend.
   * @return {@code true} if the version of the server is an OEM version and {@code false} otherwise.
   */
  public static boolean isOEMVersion()
  {
    return !isClassAvailable("org.opends.server.backends.jeb.JEBackend");
  }

  /**
   * Returns true if the class is available in the classpath.
   * @param className the string representing the class to check.
   * @return {@code true} if the class is available in the classpath and {@code false} otherwise.
   */
  public static boolean isClassAvailable(final String className)
  {
    try
    {
      Class.forName(className);
      return true;
    }
    catch (Exception e)
    {
      return false;
    }
  }
}

