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



import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

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.server.EntryCacheCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.admin.std.meta.EntryCacheCfgDefn;
import org.opends.server.api.EntryCache;
import org.opends.server.config.ConfigException;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.messages.MessageBuilder;
import org.opends.server.admin.std.server.EntryCacheMonitorProviderCfg;
import org.opends.server.api.Backend;
import org.opends.server.config.ConfigConstants;
import org.opends.server.config.ConfigEntry;
import org.opends.server.extensions.DefaultEntryCache;
import org.opends.server.monitors.EntryCacheMonitorProvider;
import org.opends.server.types.DN;

import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.messages.ExtensionMessages.*;
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>
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  // The default entry cache.
  private DefaultEntryCache _defaultEntryCache = null;

  // The entry cache order map sorted by the cache level.
  private SortedMap<Integer, EntryCache<? extends
    EntryCacheCfg>> cacheOrderMap = new TreeMap<Integer,
    EntryCache<? extends EntryCacheCfg>>();

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

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

  /**
   * Creates a new instance of this entry cache config manager.
   */
  public EntryCacheConfigManager()
  {
    // No implementation is required.
  }


  /**
   * 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)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message 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.decode(ConfigConstants.DN_ENTRY_CACHE_BASE);
      entryCacheBase   = DirectoryServer.getConfigEntry(configEntryDN);
    } catch (Exception e) {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      logError(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY.get());
      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)
    {
      logError(WARN_CONFIG_ENTRYCACHE_NO_CONFIG_ENTRY.get());
      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()) {
        if (cacheOrderMap.containsKey(configuration.getCacheLevel())) {
          // Log error and skip this cache.
          logError(ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
            String.valueOf(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) {
          logError(ie.getMessageObject());
        }
      }
    }

    // If requested preload the entry cache.
    if (rootConfiguration.getGlobalConfiguration().isEntryCachePreload() &&
        !cacheOrderMap.isEmpty()) {
      // Preload from every active public backend.
      Map<DN, Backend> baseDNMap =
        DirectoryServer.getPublicNamingContexts();
      Set<Backend> proccessedBackends = new HashSet<Backend>();
      for (Backend backend : baseDNMap.values()) {
        if (!proccessedBackends.contains(backend)) {
          proccessedBackends.add(backend);
          try {
            backend.preloadEntryCache();
          } catch (UnsupportedOperationException ex) {
            // Some backend implementations might not support entry
            // cache preload. Log a warning and continue.
            Message message = WARN_CACHE_PRELOAD_BACKEND_FAILED.get(
              backend.getBackendID());
            logError(message);
            continue;
          }
        }
      }
    }
  }


  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
      EntryCacheCfg configuration,
      List<Message> 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() &&
      (cacheNameToLevelMap.get(
       configuration.dn().toNormalizedString()) != null)) {
      int currentCacheLevel = cacheNameToLevelMap.get(
        configuration.dn().toNormalizedString());

      // 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(
            String.valueOf(configuration.dn()),
            configuration.getCacheLevel()));
        status = false;
      }
    }

    return status;
  }


  /**
   * {@inheritDoc}
   */
  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() &&
      (cacheNameToLevelMap.get(
       configuration.dn().toNormalizedString()) != null)) {
      int currentCacheLevel = cacheNameToLevelMap.get(
        configuration.dn().toNormalizedString());
      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(configuration.dn().toNormalizedString(),
          configuration.getCacheLevel());
      }
    }

    // Returned result.
    ConfigChangeResult changeResult = new ConfigChangeResult(
        ResultCode.SUCCESS, false, new ArrayList<Message>()
        );

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

    // Push any changes made to the cache order map.
    _defaultEntryCache.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 initalize it.
    try
    {
      loadAndInstallEntryCache (newClassName, configuration);
    }
    catch (InitializationException ie)
    {
      changeResult.addMessage (ie.getMessageObject());
      changeResult.setResultCode (DirectoryServer.getServerErrorResultCode());
      return changeResult;
    }

    return changeResult;
  }


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

    // Check if there is another entry cache installed at the same level.
    if (!cacheOrderMap.isEmpty()) {
      if (cacheOrderMap.containsKey(configuration.getCacheLevel())) {
        unacceptableReasons.add(
          ERR_CONFIG_ENTRYCACHE_CONFIG_LEVEL_NOT_ACCEPTABLE.get(
            String.valueOf(configuration.dn()),
            configuration.getCacheLevel()));
        status = false;
        return status;
      }
    }

    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());
        status = false;
      }
    }

    return status;
  }


  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationAdd(
      EntryCacheCfg configuration
      )
  {
    // Returned result.
    ConfigChangeResult changeResult = new ConfigChangeResult(
        ResultCode.SUCCESS, false, new ArrayList<Message>()
        );

    // 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}
   */
  public boolean isConfigurationDeleteAcceptable(
      EntryCacheCfg configuration,
      List<Message> 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}
   */
  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());
    }

    // Returned result.
    ConfigChangeResult changeResult = new ConfigChangeResult(
        ResultCode.SUCCESS, false, new ArrayList<Message>()
        );

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

      // Push any changes made to the cache order map.
      _defaultEntryCache.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().toNormalizedString(),
      configuration.getCacheLevel());

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

    // Install and register the monitor for this cache.
    EntryCacheMonitorProvider monitor =
        new EntryCacheMonitorProvider(configuration.dn().
        getRDN().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);
  }


  /**
   * 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 EntryCache<? extends EntryCacheCfg> loadEntryCache(
    String        className,
    EntryCacheCfg configuration,
    boolean initialize
    )
    throws InitializationException
  {
    EntryCache 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());
    }

    try
    {
      EntryCacheCfgDefn                   definition;
      ClassPropertyDefinition             propertyDefinition;
      Class<? extends EntryCache>         cacheClass;
      EntryCache<? extends EntryCacheCfg> cache;

      definition = EntryCacheCfgDefn.getInstance();
      propertyDefinition = definition.getJavaClassPropertyDefinition();
      cacheClass = 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.
      if (initialize || (entryCache == null)) {
        cache = (EntryCache<? extends EntryCacheCfg>) cacheClass.newInstance();
      } else {
        cache = (EntryCache<? extends EntryCacheCfg>) entryCache;
      }

      if (initialize)
      {
        Method method = cache.getClass().getMethod("initializeEntryCache",
            configuration.configurationClass());
        method.invoke(cache, 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())
      {
        Method method = cache.getClass().getMethod("isConfigurationAcceptable",
                                                   EntryCacheCfg.class,
                                                   List.class);

        List<Message> unacceptableReasons = new ArrayList<Message>();
        Boolean acceptable = (Boolean) method.invoke(cache, configuration,
                                                     unacceptableReasons);
        if (! acceptable)
        {
          MessageBuilder buffer = new MessageBuilder();
          if (! unacceptableReasons.isEmpty())
          {
            Iterator<Message> iterator = unacceptableReasons.iterator();
            buffer.append(iterator.next());
            while (iterator.hasNext())
            {
              buffer.append(".  ");
              buffer.append(iterator.next());
            }
          }

          Message message = ERR_CONFIG_ENTRYCACHE_CONFIG_NOT_ACCEPTABLE.get(
              String.valueOf(configuration.dn()), buffer.toString());
          throw new InitializationException(message);
        }
      }

      return cache;
    }
    catch (Exception e)
    {
      if (debugEnabled()) {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

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

}
