/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.plugin;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.opends.messages.Message;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.server.ServerManagementContext;
import org.opends.server.admin.std.server.FractionalLDIFImportPluginCfg;
import org.opends.server.admin.std.server.PluginCfg;
import org.opends.server.admin.std.server.ReplicationDomainCfg;
import org.opends.server.admin.std.server.ReplicationSynchronizationProviderCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.api.plugin.*;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.replication.plugin.LDAPReplicationDomain.
  AttributeValueStringIterator;
import org.opends.server.replication.plugin.LDAPReplicationDomain.
  FractionalConfig;
import org.opends.server.types.*;

import static org.opends.messages.ReplicationMessages.*;

/**
 * This class implements a Directory Server plugin that is used in fractional
 * replication to initialize a just configured fractional domain (when an online
 * full update occurs or offline/online ldif import).
 * The following tasks are done:
 * - check that the fractional configuration (if any) stored in the (incoming)
 * root entry of the domain is compliant with the fractional configuration of
 * the domain (if not make online update stop)
 * - perform filtering according to fractional configuration of the domain
 * - flush the fractional configuration of the domain in the root entry
 *  (if no one already present)
 */
public final class FractionalLDIFImportPlugin
  extends DirectoryServerPlugin<FractionalLDIFImportPluginCfg>
  implements ConfigurationChangeListener<FractionalLDIFImportPluginCfg>
{
  // Holds the fractional configuration and if available the replication domain
  // matching this import session (they form the importfractional context).
  // Domain is available if the server is online (import-ldif, online full
  // update..) otherwise, this is an import-ldif with server off. The key is the
  // ImportConfig object of the session which acts as a cookie for the whole
  // session. This allows to potentially run man imports at the same time.
  private final Hashtable<LDIFImportConfig, ImportFractionalContext>
    importSessionContexts = new Hashtable<LDIFImportConfig,
    ImportFractionalContext>();

  /**
   * Holds an import session fractional context.
   */
  private static class ImportFractionalContext
  {
    // Fractional configuration of the local domain (may be null if import on a
    // not replicated domain)
    private FractionalConfig fractionalConfig = null;
    // The local domain object (may stay null if server is offline)
    private LDAPReplicationDomain domain = null;

    /**
     * Constructor.
     * @param fractionalConfig
     * @param domain
     */
    public ImportFractionalContext(FractionalConfig fractionalConfig,
      LDAPReplicationDomain domain)
    {
      this.fractionalConfig = fractionalConfig;
      this.domain = domain;
    }

    /**
     * Getter for the fractional configuration.
     * @return the fractionalConfig
     */
    public FractionalConfig getFractionalConfig()
    {
      return fractionalConfig;
    }

    /**
     * Getter for the domain..
     * @return the domain
     */
    public LDAPReplicationDomain getDomain()
    {
      return domain;
    }
  }

  /**
   * Creates a new instance of this Directory Server plugin.  Every plugin must
   * implement a default constructor (it is the only one that will be used to
   * create plugins defined in the configuration), and every plugin constructor
   * must call {@code super()} as its first element.
   */
  public FractionalLDIFImportPlugin()
  {
    super();
  }

  /**
   * {@inheritDoc}
   */
  @Override()
  public final void initializePlugin(Set<PluginType> pluginTypes,
    FractionalLDIFImportPluginCfg configuration)
    throws ConfigException
  {
    // Make sure that the plugin has been enabled for the appropriate types.
    for (PluginType t : pluginTypes)
    {
      switch (t)
      {
        case LDIF_IMPORT:
        case LDIF_IMPORT_END:
          // This is acceptable.
          break;

        default:
          Message message =
            ERR_PLUGIN_FRACTIONAL_LDIF_IMPORT_INVALID_PLUGIN_TYPE.get(
            t.toString());
          throw new ConfigException(message);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override()
  public final void finalizePlugin()
  {
    // Nothing to do
  }

  /**
   * Attempts to retrieve the fractional configuration of the domain being
   * imported.
   * @param entry An imported entry of the imported domain
   * @return The parsed fractional configuration for the domain matching the
   * passed entry. Null if no configuration is found for the domain
   * (not a replicated domain).
   */
  private static FractionalConfig getStaticReplicationDomainFractionalConfig(
    Entry entry) throws Exception {

    // Retrieve the configuration
    ServerManagementContext context = ServerManagementContext.getInstance();
    RootCfg root = context.getRootConfiguration();


    ReplicationSynchronizationProviderCfg sync =
      (ReplicationSynchronizationProviderCfg)
      root.getSynchronizationProvider("Multimaster Synchronization");

    String[] domainNames = sync.listReplicationDomains();
    if (domainNames == null)
    {
      // No domain in replication
      return null;
    }

    // Find the configuration for domain the entry is part of
    ReplicationDomainCfg matchingReplicatedDomainCfg = null;
    for (String domainName : domainNames)
    {
      // Get the domain configuration object
      ReplicationDomainCfg replicationDomainCfg =
      sync.getReplicationDomain(domainName);
      // Is the entry a sub entry of the replicated domain main entry ?
      DN replicatedDn = replicationDomainCfg.getBaseDN();
      DN entryDn = entry.getDN();
      if (entryDn.isDescendantOf(replicatedDn))
      {
        // Found the matching replicated domain configuration object
        matchingReplicatedDomainCfg = replicationDomainCfg;
        break;
      }
    }

    if (matchingReplicatedDomainCfg == null)
    {
      // No matching replicated domain found
      return null;
    }

    // Extract the fractional configuration from the domain configuration object
    // and return it.
    return FractionalConfig.toFractionalConfig(matchingReplicatedDomainCfg);
  }

  /**
   * {@inheritDoc}
   */
  @Override()
  public final void doLDIFImportEnd(
    LDIFImportConfig importConfig)
  {
    // Remove the cookie of this import session
    synchronized(importSessionContexts)
    {
      importSessionContexts.remove(importConfig);
    }
  }

  /**
   * See class comment for what we achieve here...
   * {@inheritDoc}
   */
  @Override()
  public final PluginResult.ImportLDIF doLDIFImport(
    LDIFImportConfig importConfig, Entry entry)
  {
    /**
     * try to get the import fractional context for this entry. If not found,
     * create and initialize it. The mechanism here is done to take a lock only
     * once for the whole import session (except the necessary lock of the
     * doLDIFImportEnd method)
     */
    ImportFractionalContext importFractionalContext =
      importSessionContexts.get(importConfig);

    DN entryDn = entry.getDN();
    FractionalConfig localFractionalConfig = null;

    // If no context, create it
    if (importFractionalContext == null)
    {
      synchronized(importSessionContexts)
      {
        // Insure antoher thread was not creating the context at the same time
        // (we would create it for the second time which is useless)
        importFractionalContext = importSessionContexts.get(importConfig);
        if (importFractionalContext == null)
        {
          /*
           * Create context
           */

          /**
           * Retrieve the replicated domain this entry belongs to. Try to
           * retrieve replication domain instance first. If we are in an online
           * server, we should get it (if we are treating an entry that belongs
           * to a replicated domain), otherwise the domain is not replicated or
           * we are in an offline server context (import-ldif command run with
           * offline server) and we must retrieve the fractional configuration
           * directly from the configuration management system.
           */
          LDAPReplicationDomain domain =
            MultimasterReplication.findDomain(entryDn, null);

          // Get the fractional configuration extracted from the local server
          // configuration for the currently imported domain
          if (domain == null)
          {
            // Server may be offline, attempt to find fractional configuration
            // from config sub-system
            try
            {
              localFractionalConfig =
                getStaticReplicationDomainFractionalConfig(entry);
            } catch (Exception ex)
            {
              Message message = ERR_FRACTIONAL_COULD_NOT_RETRIEVE_CONFIG.get(
                entry.toString());
              return PluginResult.ImportLDIF.stopEntryProcessing(message);
            }
          } else
          {
            // Found a live domain, retrieve the fractional configuration from
            // it.
            localFractionalConfig = domain.getFractionalConfig();
          }
          // Create context and store it
          importFractionalContext =
            new ImportFractionalContext(localFractionalConfig, domain);
          importSessionContexts.put(importConfig, importFractionalContext);
        }
      }
    }

    // Extract the fractional configuration from the context
    localFractionalConfig = importFractionalContext.getFractionalConfig();
    if (localFractionalConfig == null)
    {
      // Not part of a replicated domain : nothing to do
      return PluginResult.ImportLDIF.continueEntryProcessing();
    }

    /**
     * At this point, either the domain instance has been found and we  use its
     * fractional configuration, or the server is offline and we use the parsed
     * fractional configuration. We differentiate both cases testing if domain
     * is null. We are also for sure handling an entry of a replicated suffix.
     */

    // Is the entry to handle the root entry of the domain ? If yes, analyze the
    // fractional configuration in it and compare with local fractional
    // configuration. Stop the import if some inconsistency is detected
    DN replicatedDomainBaseDn = localFractionalConfig.getBaseDn();
    if (replicatedDomainBaseDn.equals(entryDn))
    {
      /*
       * This is the root entry, try to read a fractional configuration from it
       */
      Iterator<String> exclIt = null;
      AttributeType fractionalExcludeType =
        DirectoryServer.getAttributeType(
        LDAPReplicationDomain.REPLICATION_FRACTIONAL_EXCLUDE);
      List<Attribute> exclAttrs =
        entry.getAttribute(fractionalExcludeType);
      Attribute exclAttr = null;
      if (exclAttrs != null)
      {
        exclAttr = exclAttrs.get(0);
        if (exclAttr != null)
        {
          exclIt = new AttributeValueStringIterator(exclAttr.iterator());
        }
      }

      Iterator<String> inclIt = null;
      AttributeType fractionalIncludeType =
        DirectoryServer.getAttributeType(
        LDAPReplicationDomain.REPLICATION_FRACTIONAL_INCLUDE);
      List<Attribute> inclAttrs =
        entry.getAttribute(fractionalIncludeType);
      Attribute inclAttr = null;
      if (inclAttrs != null)
      {
        inclAttr = inclAttrs.get(0);
        if (inclAttr != null)
        {
          inclIt = new AttributeValueStringIterator(inclAttr.iterator());
        }
      }

      // Compare backend and local fractional configuration
      boolean sameConfig = LDAPReplicationDomain.
        isFractionalConfigConsistent(localFractionalConfig, exclIt, inclIt);
      if (localFractionalConfig.isFractional())
      {
        // Local domain is fractional
        if (sameConfig)
        {
          // Both local and remote fractional configuration are equivalent :
          // follow import, no need to go with filtering as remote backend
          // should be ok
          return PluginResult.ImportLDIF.continueEntryProcessing();
        } else
        {
          // Local domain is fractional, remote domain has not same config
          boolean remoteDomainHasSomeConfig = false;
          if ((exclAttr != null && (exclAttr.size() > 0)) ||
            (inclAttr != null && (inclAttr.size() > 0)))
          {
            remoteDomainHasSomeConfig = true;
          }
          if (remoteDomainHasSomeConfig)
          {
            LDAPReplicationDomain domain = importFractionalContext.getDomain();
            if (domain != null)
            {
              // Local domain is fractional, remote domain has some config which
              // is different : stop import (error will be logged when import is
              // stopped)
              domain.setImportErrorMessageId(
                LDAPReplicationDomain.IMPORT_ERROR_MESSAGE_BAD_REMOTE);
              domain.setFollowImport(false);
              return PluginResult.ImportLDIF.stopEntryProcessing(null);
            } else
            {
              Message message = NOTE_ERR_LDIF_IMPORT_FRACTIONAL_BAD_DATA_SET.
                get(replicatedDomainBaseDn.toString());
              return PluginResult.ImportLDIF.stopEntryProcessing(message);
            }
          } else
          {
            // Local domain is fractional but remote domain has no config :
            // flush local config into root entry and follow import with
            // filtering
            flushFractionalConfigIntoEntry(localFractionalConfig, entry);
          }
        }
      } else
      {
        // Local domain is not fractional
        if (sameConfig)
        {
          // None of the local or remote domain has fractional config : nothing
          // more to do : let import finish
          return PluginResult.ImportLDIF.continueEntryProcessing();
        } else
        {
          LDAPReplicationDomain domain = importFractionalContext.getDomain();
          if (domain != null)
          {
            // Local domain is not fractional but remote one is : stop import :
            // local domain should be configured with the same config as remote
            // one
            domain.setImportErrorMessageId(
                LDAPReplicationDomain.
                IMPORT_ERROR_MESSAGE_REMOTE_IS_FRACTIONAL);
            domain.setFollowImport(false);
            return PluginResult.ImportLDIF.stopEntryProcessing(null);
          } else
          {
            Message message =
              NOTE_ERR_LDIF_IMPORT_FRACTIONAL_DATA_SET_IS_FRACTIONAL.get(
                replicatedDomainBaseDn.toString());
            return PluginResult.ImportLDIF.stopEntryProcessing(message);
          }
        }
      }
    }

    // If we get here, local domain fractional configuration is enabled.
    // Now filter for potential attributes to be removed.
    LDAPReplicationDomain.fractionalRemoveAttributesFromEntry(
      localFractionalConfig, entry.getDN().getRDN(),
      entry.getObjectClasses(), entry.getUserAttributes(), true);

    return PluginResult.ImportLDIF.continueEntryProcessing();
  }

  /**
   * Write the fractional configuration in the passed domain into the passed
   * entry.
   * WARNING: assumption is that no fractional attributes at all is already
   * present in the passed entry. Also assumption is that domain fractional
   * configuration is on.
   * @param localFractionalConfig The local domain fractional configuration
   * @param entry The entry to modify
   */
  private static void flushFractionalConfigIntoEntry(FractionalConfig
    localFractionalConfig, Entry entry)
  {
    if (localFractionalConfig.isFractional()) // Paranoia check
    {
      // Get the fractional configuration of the domain
      boolean fractionalExclusive =
        localFractionalConfig.isFractionalExclusive();
      Map<String, List<String>> fractionalSpecificClassesAttributes =
        localFractionalConfig.getFractionalSpecificClassesAttributes();
      List<String> fractionalAllClassesAttributes =
        localFractionalConfig.getFractionalAllClassesAttributes();

      // Create attribute builder for the rigth fractional mode
      String fractAttribute = null;
      if (fractionalExclusive)
      {
        fractAttribute = LDAPReplicationDomain.REPLICATION_FRACTIONAL_EXCLUDE;
      } else
      {
        fractAttribute = LDAPReplicationDomain.REPLICATION_FRACTIONAL_INCLUDE;
      }
      AttributeBuilder attrBuilder = new AttributeBuilder(fractAttribute);
      boolean somethingToFlush = false;

      // Add attribute values for all classes
      int size = fractionalAllClassesAttributes.size();
      if (size > 0)
      {
        String fracValue = "*:";
        int i = 1;
        for (String attrName : fractionalAllClassesAttributes)
        {
          fracValue += attrName;
          if (i < size)
          {
            fracValue += ",";
          }
          i++;
        }
        somethingToFlush = true;
        attrBuilder.add(fracValue);
      }

      // Add attribute values for specific classes
      size = fractionalSpecificClassesAttributes.size();
      if (size > 0)
      {
        for (String className : fractionalSpecificClassesAttributes.keySet())
        {
          int valuesSize =
            fractionalSpecificClassesAttributes.get(className).size();
          if (valuesSize > 0)
          {
            String fracValue = className + ":";
            int i = 1;
            for (String attrName : fractionalSpecificClassesAttributes.get(
              className))
            {
              fracValue += attrName;
              if (i < valuesSize)
              {
                fracValue += ",";
              }
              i++;
            }
            somethingToFlush = true;
            attrBuilder.add(fracValue);
          }
        }
      }

      // Now flush attribute values into entry
      if (somethingToFlush)
      {
        List<AttributeValue> duplicateValues = new ArrayList<AttributeValue>();
        entry.addAttribute(attrBuilder.toAttribute(), duplicateValues);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isConfigurationAcceptable(PluginCfg configuration,
    List<Message> unacceptableReasons)
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
    FractionalLDIFImportPluginCfg configuration,
    List<Message> unacceptableReasons)
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
    FractionalLDIFImportPluginCfg configuration)
  {
    return new ConfigChangeResult(ResultCode.SUCCESS, false);
  }
}

