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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.api.ConfigAddListener;
import org.opends.server.api.ConfigChangeListener;
import org.opends.server.api.ConfigDeleteListener;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ObjectClass;

import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * This class defines a configuration entry, which can hold zero or more
 * attributes that may control the configuration of various components of the
 * Directory Server.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=true,
     mayExtend=false,
     mayInvoke=true)
public final class ConfigEntry
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();




  /** The set of immediate children for this configuration entry. */
  private final ConcurrentMap<DN,ConfigEntry> children;

  /** The immediate parent for this configuration entry. */
  private ConfigEntry parent;

  /** The set of add listeners that have been registered with this entry. */
  private final CopyOnWriteArrayList<ConfigAddListener> addListeners;

  /** The set of change listeners that have been registered with this entry. */
  private final CopyOnWriteArrayList<ConfigChangeListener> changeListeners;

  /** The set of delete listeners that have been registered with this entry. */
  private final CopyOnWriteArrayList<ConfigDeleteListener> deleteListeners;

  /** The actual entry wrapped by this configuration entry. */
  private Entry entry;

  /** The lock used to provide threadsafe access to this configuration entry. */
  private Object entryLock;



  /**
   * Creates a new config entry with the provided information.
   *
   * @param  entry   The entry that will be encapsulated by this config entry.
   * @param  parent  The configuration entry that is the immediate parent for
   *                 this configuration entry.  It may be <CODE>null</CODE> if
   *                 this entry is the configuration root.
   */
  public ConfigEntry(Entry entry, ConfigEntry parent)
  {
    this.entry  = entry;
    this.parent = parent;

    children        = new ConcurrentHashMap<>();
    addListeners    = new CopyOnWriteArrayList<>();
    changeListeners = new CopyOnWriteArrayList<>();
    deleteListeners = new CopyOnWriteArrayList<>();
    entryLock       = new Object();
  }



  /**
   * Retrieves the actual entry wrapped by this configuration entry.
   *
   * @return  The actual entry wrapped by this configuration entry.
   */
  public Entry getEntry()
  {
    return entry;
  }



  /**
   * Replaces the actual entry wrapped by this configuration entry with the
   * provided entry.  The given entry must be non-null and must have the same DN
   * as the current entry.  No validation will be performed on the target entry.
   * All add/delete/change listeners that have been registered will be
   * maintained, it will keep the same parent and set of children, and all other
   * settings will remain the same.
   *
   * @param  entry   The new entry to store in this config entry.
   */
  public void setEntry(Entry entry)
  {
    synchronized (entryLock)
    {
      this.entry = entry;
    }
  }



  /**
   * Retrieves the DN for this configuration entry.
   *
   * @return  The DN for this configuration entry.
   */
  public DN getDN()
  {
    return entry.getName();
  }



  /**
   * Indicates whether this configuration entry contains the specified
   * objectclass.
   *
   * @param  name  The name of the objectclass for which to make the
   *               determination.
   *
   * @return  <CODE>true</CODE> if this configuration entry contains the
   *          specified objectclass, or <CODE>false</CODE> if not.
   */
  public boolean hasObjectClass(String name)
  {
    ObjectClass oc = DirectoryServer.getObjectClass(name.toLowerCase());
    if (oc == null)
    {
      oc = DirectoryServer.getDefaultObjectClass(name);
    }

    return entry.hasObjectClass(oc);
  }



  /**
   * Retrieves the specified configuration attribute from this configuration
   * entry.
   *
   * @param  stub  The stub to use to format the returned configuration
   *               attribute.
   *
   * @return  The requested configuration attribute from this configuration
   *          entry, or <CODE>null</CODE> if no such attribute is present in
   *          this entry.
   *
   * @throws  ConfigException  If the specified attribute exists but cannot be
   *                           interpreted as the specified type of
   *                           configuration attribute.
   */
  public ConfigAttribute getConfigAttribute(ConfigAttribute stub)
         throws ConfigException
  {
    String attrName = stub.getName();
    AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(attrName.toLowerCase(), attrName);
    List<Attribute> attrList = entry.getAttribute(attrType);
    if (attrList != null && !attrList.isEmpty())
    {
      return stub.getConfigAttribute(attrList);
    }
    return null;
  }



  /**
   * Puts the provided configuration attribute in this entry (adding a new
   * attribute if one doesn't exist, or replacing it if one does).  This must
   * only be performed on a duplicate of a configuration entry and never on a
   * configuration entry itself.
   *
   * @param  attribute  The configuration attribute to use.
   */
  public void putConfigAttribute(ConfigAttribute attribute)
  {
    String name = attribute.getName();
    AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(
        name.toLowerCase(), name, attribute.getSyntax());

    List<Attribute> attrs = new ArrayList<>(2);
    AttributeBuilder builder = new AttributeBuilder(attrType, name);
    builder.addAll(attribute.getActiveValues());
    attrs.add(builder.toAttribute());
    if (attribute.hasPendingValues())
    {
      builder = new AttributeBuilder(attrType, name);
      builder.setOption(OPTION_PENDING_VALUES);
      builder.addAll(attribute.getPendingValues());
      attrs.add(builder.toAttribute());
    }

    entry.putAttribute(attrType, attrs);
  }



  /**
   * Removes the specified configuration attribute from the entry.  This will
   * have no impact if the specified attribute is not contained in the entry.
   *
   * @param  lowerName  The name of the configuration attribute to remove from
   *                    the entry, formatted in all lowercase characters.
   *
   * @return  <CODE>true</CODE> if the requested attribute was found and
   *          removed, or <CODE>false</CODE> if not.
   */
  public boolean removeConfigAttribute(String lowerName)
  {
    for (AttributeType t : entry.getUserAttributes().keySet())
    {
      if (t.hasNameOrOID(lowerName))
      {
        entry.getUserAttributes().remove(t);
        return true;
      }
    }

    for (AttributeType t : entry.getOperationalAttributes().keySet())
    {
      if (t.hasNameOrOID(lowerName))
      {
        entry.getOperationalAttributes().remove(t);
        return true;
      }
    }

    return false;
  }



  /**
   * Retrieves the configuration entry that is the immediate parent for this
   * configuration entry.
   *
   * @return  The configuration entry that is the immediate parent for this
   *          configuration entry.  It may be <CODE>null</CODE> if this entry is
   *          the configuration root.
   */
  public ConfigEntry getParent()
  {
    return parent;
  }



  /**
   * Retrieves the set of children associated with this configuration entry.
   * This list should not be altered by the caller.
   *
   * @return  The set of children associated with this configuration entry.
   */
  public ConcurrentMap<DN, ConfigEntry> getChildren()
  {
    return children;
  }



  /**
   * Indicates whether this entry has any children.
   *
   * @return  <CODE>true</CODE> if this entry has one or more children, or
   *          <CODE>false</CODE> if not.
   */
  public boolean hasChildren()
  {
    return !children.isEmpty();
  }



  /**
   * Adds the specified entry as a child of this configuration entry.  No check
   * will be made to determine whether the specified entry actually should be a
   * child of this entry, and this method will not notify any add listeners that
   * might be registered with this configuration entry.
   *
   * @param  childEntry  The entry to add as a child of this configuration
   *                     entry.
   *
   * @throws  ConfigException  If the provided entry could not be added as a
   *                           child of this configuration entry (e.g., because
   *                           another entry already exists with the same DN).
   */
  public void addChild(ConfigEntry childEntry)
         throws ConfigException
  {
    ConfigEntry conflictingChild;

    synchronized (entryLock)
    {
      conflictingChild = children.putIfAbsent(childEntry.getDN(), childEntry);
    }

    if (conflictingChild != null)
    {
      throw new ConfigException(ERR_CONFIG_ENTRY_CONFLICTING_CHILD.get(
          conflictingChild.getDN(), entry.getName()));
    }
  }



  /**
   * Attempts to remove the child entry with the specified DN.  This method will
   * not notify any delete listeners that might be registered with this
   * configuration entry.
   *
   * @param  childDN  The DN of the child entry to remove from this config
   *                  entry.
   *
   * @return  The configuration entry that was removed as a child of this
   *          entry.
   *
   * @throws  ConfigException  If the specified child entry did not exist or if
   *                           it had children of its own.
   */
  public ConfigEntry removeChild(DN childDN)
         throws ConfigException
  {
    synchronized (entryLock)
    {
      try
      {
        ConfigEntry childEntry = children.get(childDN);
        if (childEntry == null)
        {
          throw new ConfigException(ERR_CONFIG_ENTRY_NO_SUCH_CHILD.get(
              childDN, entry.getName()));
        }

        if (childEntry.hasChildren())
        {
          throw new ConfigException(ERR_CONFIG_ENTRY_CANNOT_REMOVE_NONLEAF.get(
              childDN, entry.getName()));
        }

        children.remove(childDN);
        return childEntry;
      }
      catch (ConfigException ce)
      {
        throw ce;
      }
      catch (Exception e)
      {
        logger.traceException(e);

        LocalizableMessage message = ERR_CONFIG_ENTRY_CANNOT_REMOVE_CHILD.
            get(childDN, entry.getName(), stackTraceToSingleLineString(e));
        throw new ConfigException(message, e);
      }
    }
  }



  /**
   * Creates a duplicate of this configuration entry that should be used when
   * making changes to this entry.  Changes should only be made to the duplicate
   * (never the original) and then applied to the original.  Note that this
   * method and the other methods used to make changes to the entry contents are
   * not threadsafe and therefore must be externally synchronized to ensure that
   * only one change may be in progress at any given time.
   *
   * @return  A duplicate of this configuration entry that should be used when
   *          making changes to this entry.
   */
  public ConfigEntry duplicate()
  {
    return new ConfigEntry(entry.duplicate(false), parent);
  }



  /**
   * Retrieves the set of change listeners that have been registered with this
   * configuration entry.
   *
   * @return  The set of change listeners that have been registered with this
   *          configuration entry.
   */
  public CopyOnWriteArrayList<ConfigChangeListener> getChangeListeners()
  {
    return changeListeners;
  }



  /**
   * Registers the provided change listener so that it will be notified of any
   * changes to this configuration entry.  No check will be made to determine
   * whether the provided listener is already registered.
   *
   * @param  listener  The change listener to register with this config entry.
   */
  public void registerChangeListener(ConfigChangeListener listener)
  {
    changeListeners.add(listener);
  }



  /**
   * Attempts to deregister the provided change listener with this configuration
   * entry.
   *
   * @param  listener  The change listener to deregister with this config entry.
   *
   * @return  <CODE>true</CODE> if the specified listener was deregistered, or
   *          <CODE>false</CODE> if it was not.
   */
  public boolean deregisterChangeListener(ConfigChangeListener listener)
  {
    return changeListeners.remove(listener);
  }



  /**
   * Retrieves the set of config add listeners that have been registered for
   * this entry.
   *
   * @return  The set of config add listeners that have been registered for this
   *          entry.
   */
  public CopyOnWriteArrayList<ConfigAddListener> getAddListeners()
  {
    return addListeners;
  }



  /**
   * Registers the provided add listener so that it will be notified if any new
   * entries are added immediately below this configuration entry.
   *
   * @param  listener  The add listener that should be registered.
   */
  public void registerAddListener(ConfigAddListener listener)
  {
    addListeners.addIfAbsent(listener);
  }



  /**
   * Deregisters the provided add listener so that it will no longer be
   * notified if any new entries are added immediately below this configuration
   * entry.
   *
   * @param  listener  The add listener that should be deregistered.
   */
  public void deregisterAddListener(ConfigAddListener listener)
  {
    addListeners.remove(listener);
  }



  /**
   * Retrieves the set of config delete listeners that have been registered for
   * this entry.
   *
   * @return  The set of config delete listeners that have been registered for
   *          this entry.
   */
  public CopyOnWriteArrayList<ConfigDeleteListener> getDeleteListeners()
  {
    return deleteListeners;
  }



  /**
   * Registers the provided delete listener so that it will be notified if any
   * entries are deleted immediately below this configuration entry.
   *
   * @param  listener  The delete listener that should be registered.
   */
  public void registerDeleteListener(ConfigDeleteListener listener)
  {
    deleteListeners.addIfAbsent(listener);
  }



  /**
   * Deregisters the provided delete listener so that it will no longer be
   * notified if any new are removed immediately below this configuration entry.
   *
   * @param  listener  The delete listener that should be deregistered.
   */
  public void deregisterDeleteListener(ConfigDeleteListener listener)
  {
    deleteListeners.remove(listener);
  }

  /** {@inheritDoc} */
  @Override
  public String toString()
  {
    return entry.getName().toString();
  }
}
