/*
 * 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 2013-2015 ForgeRock AS.
 */
package org.opends.server.core;

import java.util.*;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.Utils;
import org.opends.server.admin.ClassPropertyDefinition;
import org.opends.server.admin.server.ConfigurationAddListener;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.server.ConfigurationDeleteListener;
import org.opends.server.admin.server.ServerManagementContext;
import org.opends.server.admin.std.meta.EntryCacheCfgDefn;
import org.opends.server.admin.std.server.EntryCacheCfg;
import org.opends.server.admin.std.server.EntryCacheMonitorProviderCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.api.EntryCache;
import org.opends.server.config.ConfigConstants;
import org.opends.server.config.ConfigEntry;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.extensions.DefaultEntryCache;
import org.opends.server.monitors.EntryCacheMonitorProvider;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.InitializationException;

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

/**
 * This class defines a utility that will be used to manage the configuration
 * for the Directory Server entry cache.  The default entry cache is always
 * enabled.
 */
public class EntryCacheConfigManager
       implements
          ConfigurationChangeListener <EntryCacheCfg>,
          ConfigurationAddListener    <EntryCacheCfg>,
          ConfigurationDeleteListener <EntryCacheCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The default entry cache. */
  private DefaultEntryCache _defaultEntryCache;

  /** The entry cache order map sorted by the cache level. */
  @SuppressWarnings("rawtypes")
  private SortedMap<Integer, EntryCache> cacheOrderMap = new TreeMap<>();

  /** The entry cache to level map. */
  private Map<DN,Integer> cacheNameToLevelMap = new HashMap<>();

  /** Global entry cache monitor provider name. */
  private static final String
    DEFAULT_ENTRY_CACHE_MONITOR_PROVIDER = "Entry Caches";

  private final ServerContext serverContext;

  /**
   * Creates a new instance of this entry cache config manager.
   *
   * @param serverContext
   *          The server context.
   */
  public EntryCacheConfigManager(ServerContext serverContext)
  {
    this.serverContext = serverContext;
  }


  /**
   * Initializes the default entry cache.
   * This should only be called at Directory Server startup.
   *
   * @throws  InitializationException  If a problem occurs while trying to
   *                                   install the default entry cache.
   */
  public void initializeDefaultEntryCache()
         throws InitializationException
  {
    try
    {
      DefaultEntryCache defaultCache = new DefaultEntryCache();
      defaultCache.initializeEntryCache(null);
      DirectoryServer.setEntryCache(defaultCache);
      _defaultEntryCache = defaultCache;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CANNOT_INSTALL_DEFAULT_CACHE.get(
          stackTraceToSingleLineString(e));
      throw new InitializationException(message, e);
    }

  }


  /**
   * Initializes the configuration associated with the Directory Server entry
   * cache.  This should only be called at Directory Server startup.  If an
   * error occurs, then a message will be logged for each entry cache that is
   * failed to initialize.
   *
   * @throws  ConfigException  If a configuration problem causes the entry
   *                           cache initialization process to fail.
   */
  public void initializeEntryCache()
         throws ConfigException
  {
    // Get the root configuration object.
    ServerManagementContext managementContext =
      ServerManagementContext.getInstance();
    RootCfg rootConfiguration =
      managementContext.getRootConfiguration();

    // Default entry cache should be already installed with
    // <CODE>initializeDefaultEntryCache()</CODE> method so
    // that there will be one even if we encounter a problem later.

    // Register as an add and delete listener with the root configuration so we
    // can be notified if any entry cache entry is added or removed.
    rootConfiguration.addEntryCacheAddListener(this);
    rootConfiguration.addEntryCacheDeleteListener(this);

    // Get the base entry cache configuration entry.
    ConfigEntry entryCacheBase;
    try {
      DN configEntryDN = DN.valueOf(ConfigConstants.DN_ENTRY_CACHE_BASE);
      entryCacheBase   = DirectoryServer.getConfigEntry(configEntryDN);
    } catch (Exception e) {
      logger.traceException(e);

      logger.warn(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY);
      return;
    }

    // If the configuration base entry is null, then assume it doesn't exist.
    // At least that entry must exist in the configuration, even if there are
    // no entry cache defined below it.
    if (entryCacheBase == null)
    {
      logger.error(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY);
      return;
    }

    // Initialize every entry cache configured.
    for (String cacheName : rootConfiguration.listEntryCaches())
    {
      // Get the entry cache configuration.
      EntryCacheCfg configuration = rootConfiguration.getEntryCache(cacheName);

      // At this point, we have a configuration entry. Register a change
      // listener with it so we can be notified of changes to it over time.
      configuration.addChangeListener(this);

      // Check if there is another entry cache installed at the same level.
      if (!cacheOrderMap.isEmpty()
          && cacheOrderMap.containsKey(configuration.getCacheLevel()))
      {
        // Log error and skip this cache.
        logger.error(ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE,
            configuration.dn(), configuration.getCacheLevel());
        continue;
      }

      // Initialize the entry cache.
      if (configuration.isEnabled()) {
        // Load the entry cache implementation class and install the entry
        // cache with the server.
        String className = configuration.getJavaClass();
        try {
          loadAndInstallEntryCache(className, configuration);
        } catch (InitializationException ie) {
          logger.error(ie.getMessageObject());
        }
      }
    }
  }


  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(
      EntryCacheCfg configuration,
      List<LocalizableMessage> unacceptableReasons
      )
  {
    // returned status -- all is fine by default
    boolean status = true;

    // Get the name of the class and make sure we can instantiate it as an
    // entry cache.
    String className = configuration.getJavaClass();
    try {
      // Load the class but don't initialize it.
      loadEntryCache(className, configuration, false);
    } catch (InitializationException ie) {
      unacceptableReasons.add(ie.getMessageObject());
      status = false;
    }

    if (!cacheOrderMap.isEmpty() && !cacheNameToLevelMap.isEmpty())
    {
      final ByteString normDN = configuration.dn().toNormalizedByteString();
      if (cacheNameToLevelMap.containsKey(normDN)) {
        int currentCacheLevel = cacheNameToLevelMap.get(normDN);

        // Check if there any existing cache at the same level.
        if (currentCacheLevel != configuration.getCacheLevel() &&
          cacheOrderMap.containsKey(configuration.getCacheLevel())) {
          unacceptableReasons.add(
            ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
              configuration.dn(), configuration.getCacheLevel()));
          status = false;
        }
      }
    }

    return status;
  }


  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(
      EntryCacheCfg configuration
      )
  {
    EntryCache<? extends EntryCacheCfg> entryCache = null;

    // If we this entry cache is already installed and active it
    // should be present in the cache maps, if so use it.
    if (!cacheOrderMap.isEmpty() && !cacheNameToLevelMap.isEmpty()) {
      final DN dn = configuration.dn();
      if (cacheNameToLevelMap.containsKey(dn))
      {
        int currentCacheLevel = cacheNameToLevelMap.get(dn);
        entryCache = cacheOrderMap.get(currentCacheLevel);

        // Check if the existing cache just shifted its level.
        if (currentCacheLevel != configuration.getCacheLevel()) {
          // Update the maps then.
          cacheOrderMap.remove(currentCacheLevel);
          cacheOrderMap.put(configuration.getCacheLevel(), entryCache);
          cacheNameToLevelMap.put(dn, configuration.getCacheLevel());
        }
      }
    }

    final ConfigChangeResult changeResult = new ConfigChangeResult();

    // If an entry cache was installed then remove it.
    if (!configuration.isEnabled())
    {
      configuration.getCacheLevel();
      if (entryCache != null)
      {
        EntryCacheMonitorProvider monitor = entryCache.getEntryCacheMonitor();
        if (monitor != null)
        {
          DirectoryServer.deregisterMonitorProvider(monitor);
          monitor.finalizeMonitorProvider();
          entryCache.setEntryCacheMonitor(null);
        }
        entryCache.finalizeEntryCache();
        cacheOrderMap.remove(configuration.getCacheLevel());
        entryCache = null;
      }
      return changeResult;
    }

    // Push any changes made to the cache order map.
    setCacheOrder(cacheOrderMap);

    // At this point, new configuration is enabled...
    // If the current entry cache is already enabled then we don't do
    // anything unless the class has changed in which case we should
    // indicate that administrative action is required.
    String newClassName = configuration.getJavaClass();
    if ( entryCache != null)
    {
      String curClassName = entryCache.getClass().getName();
      boolean classIsNew = !newClassName.equals(curClassName);
      if (classIsNew)
      {
        changeResult.setAdminActionRequired (true);
      }
      return changeResult;
    }

    // New entry cache is enabled and there were no previous one.
    // Instantiate the new class and initialize it.
    try
    {
      loadAndInstallEntryCache (newClassName, configuration);
    }
    catch (InitializationException ie)
    {
      changeResult.addMessage (ie.getMessageObject());
      changeResult.setResultCode (DirectoryServer.getServerErrorResultCode());
      return changeResult;
    }

    return changeResult;
  }


  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationAddAcceptable(
      EntryCacheCfg configuration,
      List<LocalizableMessage> unacceptableReasons
      )
  {
    // returned status -- all is fine by default
    // Check if there is another entry cache installed at the same level.
    if (!cacheOrderMap.isEmpty()
        && cacheOrderMap.containsKey(configuration.getCacheLevel()))
    {
      unacceptableReasons.add(ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
          configuration.dn(), configuration.getCacheLevel()));
      return false;
    }

    if (configuration.isEnabled())
    {
      // Get the name of the class and make sure we can instantiate it as
      // an entry cache.
      String className = configuration.getJavaClass();
      try
      {
        // Load the class but don't initialize it.
        loadEntryCache(className, configuration, false);
      }
      catch (InitializationException ie)
      {
        unacceptableReasons.add (ie.getMessageObject());
        return false;
      }
    }

    return true;
  }


  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationAdd(EntryCacheCfg configuration)
  {
    final ConfigChangeResult changeResult = new ConfigChangeResult();

    // Register a change listener with it so we can be notified of changes
    // to it over time.
    configuration.addChangeListener(this);

    if (configuration.isEnabled())
    {
      // Instantiate the class as an entry cache and initialize it.
      String className = configuration.getJavaClass();
      try
      {
        loadAndInstallEntryCache (className, configuration);
      }
      catch (InitializationException ie)
      {
        changeResult.addMessage (ie.getMessageObject());
        changeResult.setResultCode (DirectoryServer.getServerErrorResultCode());
        return changeResult;
      }
    }

    return changeResult;
  }


  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationDeleteAcceptable(
      EntryCacheCfg configuration,
      List<LocalizableMessage> unacceptableReasons
      )
  {
    // If we've gotten to this point, then it is acceptable as far as we are
    // concerned.  If it is unacceptable according to the configuration, then
    // the entry cache itself will make that determination.
    return true;
  }


  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationDelete(
      EntryCacheCfg configuration
      )
  {
    EntryCache<? extends EntryCacheCfg> entryCache = null;

    // If we this entry cache is already installed and active it
    // should be present in the current cache order map, use it.
    if (!cacheOrderMap.isEmpty()) {
      entryCache = cacheOrderMap.get(configuration.getCacheLevel());
    }

    final ConfigChangeResult changeResult = new ConfigChangeResult();

    // If the entry cache was installed then remove it.
    if (entryCache != null)
    {
      EntryCacheMonitorProvider monitor = entryCache.getEntryCacheMonitor();
      if (monitor != null)
      {
        DirectoryServer.deregisterMonitorProvider(monitor);
        monitor.finalizeMonitorProvider();
        entryCache.setEntryCacheMonitor(null);
      }
      entryCache.finalizeEntryCache();
      cacheOrderMap.remove(configuration.getCacheLevel());
      cacheNameToLevelMap.remove(configuration.dn().toNormalizedByteString());

      // Push any changes made to the cache order map.
      setCacheOrder(cacheOrderMap);

      entryCache = null;
    }

    return changeResult;
  }


  /**
   * Loads the specified class, instantiates it as an entry cache,
   * and optionally initializes that instance. Any initialize entry
   * cache is registered in the server.
   *
   * @param  className      The fully-qualified name of the entry cache
   *                        class to load, instantiate, and initialize.
   * @param  configuration  The configuration to use to initialize the
   *                        entry cache, or {@code null} if the
   *                        entry cache should not be initialized.
   *
   * @throws  InitializationException  If a problem occurred while attempting
   *                                   to initialize the entry cache.
   */
  private void loadAndInstallEntryCache(
    String        className,
    EntryCacheCfg configuration
    )
    throws InitializationException
  {
    // Get the root configuration object.
    ServerManagementContext managementContext =
      ServerManagementContext.getInstance();
    RootCfg rootConfiguration =
      managementContext.getRootConfiguration();

    // Load the entry cache class...
    EntryCache<? extends EntryCacheCfg> entryCache =
      loadEntryCache (className, configuration, true);

    // ... and install the entry cache in the server.

    // Add this entry cache to the current cache config maps.
    cacheOrderMap.put(configuration.getCacheLevel(), entryCache);
    cacheNameToLevelMap.put(configuration.dn(), configuration.getCacheLevel());

    // Push any changes made to the cache order map.
    setCacheOrder(cacheOrderMap);

    // Install and register the monitor for this cache.
    EntryCacheMonitorProvider monitor =
        new EntryCacheMonitorProvider(configuration.dn().
        rdn().getAttributeValue(0).toString(), entryCache);
    try {
      monitor.initializeMonitorProvider((EntryCacheMonitorProviderCfg)
        rootConfiguration.getMonitorProvider(
        DEFAULT_ENTRY_CACHE_MONITOR_PROVIDER));
    } catch (ConfigException ce) {
      // ConfigException here means that either the entry cache monitor
      // config entry is not present or the monitor is not enabled. In
      // either case that means no monitor provider for this cache.
      return;
    }
    entryCache.setEntryCacheMonitor(monitor);
    DirectoryServer.registerMonitorProvider(monitor);
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private void setCacheOrder(SortedMap<Integer, EntryCache> cacheOrderMap)
  {
    _defaultEntryCache.setCacheOrder((SortedMap) cacheOrderMap);
  }

  /**
   * Loads the specified class, instantiates it as an entry cache, and
   * optionally initializes that instance.
   *
   * @param  className      The fully-qualified name of the entry cache class
   *                        to load, instantiate, and initialize.
   * @param  configuration  The configuration to use to initialize the entry
   *                        cache.  It must not be {@code null}.
   * @param  initialize     Indicates whether the entry cache instance should be
   *                        initialized.
   *
   * @return  The possibly initialized entry cache.
   *
   * @throws  InitializationException  If a problem occurred while attempting
   *                                   to initialize the entry cache.
   */
  private <T extends EntryCacheCfg> EntryCache<T> loadEntryCache(
    String        className,
    T configuration,
    boolean initialize
    )
    throws InitializationException
  {
    // If we this entry cache is already installed and active it
    // should be present in the current cache order map, use it.
    EntryCache<T> entryCache = null;
    if (!cacheOrderMap.isEmpty()) {
      entryCache = cacheOrderMap.get(configuration.getCacheLevel());
    }

    try
    {
      EntryCacheCfgDefn definition = EntryCacheCfgDefn.getInstance();
      ClassPropertyDefinition propertyDefinition = definition
          .getJavaClassPropertyDefinition();
      @SuppressWarnings("unchecked")
      Class<? extends EntryCache<T>> cacheClass =
          (Class<? extends EntryCache<T>>) propertyDefinition
              .loadClass(className, EntryCache.class);

      // If there is some entry cache instance already initialized work with
      // it instead of creating a new one unless explicit init is requested.
      EntryCache<T> cache;
      if (initialize || entryCache == null) {
        cache = cacheClass.newInstance();
      } else {
        cache = entryCache;
      }

      if (initialize)
      {
        cache.initializeEntryCache(configuration);
      }
      // This will check if configuration is acceptable on disabled
      // and uninitialized cache instance that has no "acceptable"
      // change listener registered to invoke and verify on its own.
      else if (!configuration.isEnabled())
      {
        List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
        if (!cache.isConfigurationAcceptable(configuration, unacceptableReasons))
        {
          String buffer = Utils.joinAsString(".  ", unacceptableReasons);
          throw new InitializationException(
              ERR_CONFIG_ENTRYCACHE_CONFIG_NOT_ACCEPTABLE.get(configuration.dn(), buffer));
        }
      }

      return cache;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      if (!initialize) {
        if (e instanceof InitializationException) {
          throw (InitializationException) e;
        } else {
          LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CONFIG_NOT_ACCEPTABLE.get(
            configuration.dn(), e.getCause() != null ?
              e.getCause().getMessage() : stackTraceToSingleLineString(e));
          throw new InitializationException(message);
        }
      }
      LocalizableMessage message = ERR_CONFIG_ENTRYCACHE_CANNOT_INITIALIZE_CACHE.get(
        className, e.getCause() != null ? e.getCause().getMessage() :
          stackTraceToSingleLineString(e));
      throw new InitializationException(message, e);
    }
  }

}
