/*
 * 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 2008-2010 Sun Microsystems, Inc.
 */

package org.opends.admin.ads;

import static org.opends.messages.QuickSetupMessages.
 INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER;
import static org.opends.messages.QuickSetupMessages.
 INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapName;

import org.opends.admin.ads.ADSContext.ServerProperty;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.admin.ads.util.ConnectionUtils;
import org.opends.admin.ads.util.PreferredConnection;
import org.opends.admin.ads.util.ServerLoader;
import org.opends.messages.Message;
import org.opends.quicksetup.util.Utils;

/**
 * This class allows to read the configuration of the different servers that
 * are registered in a given ADS server.  It provides a read only view of the
 * configuration of the servers and of the replication topologies that might
 * be configured between them.
 */
public class TopologyCache
{
  private ADSContext adsContext;
  private ApplicationTrustManager trustManager;
  private int timeout;
  private String dn;
  private String pwd;
  private Set<ServerDescriptor> servers = new HashSet<ServerDescriptor>();
  private Set<SuffixDescriptor> suffixes = new HashSet<SuffixDescriptor>();
  private LinkedHashSet<PreferredConnection> preferredConnections =
    new LinkedHashSet<PreferredConnection>();
  private TopologyCacheFilter filter = new TopologyCacheFilter();

  private final boolean isMultiThreaded = true;
  private final static int MULTITHREAD_TIMEOUT = 90 * 1000;

  private static final Logger LOG =
    Logger.getLogger(TopologyCache.class.getName());


  /**
   * Constructor of the TopologyCache.
   * @param adsContext the adsContext to the ADS registry.
   * @param trustManager the ApplicationTrustManager that must be used to trust
   * certificates when we create connections to the registered servers to read
   * their configuration.
   * @param timeout the timeout to establish the connection in milliseconds.
   * Use {@code 0} to express no timeout.
   */
  public TopologyCache(ADSContext adsContext,
      ApplicationTrustManager trustManager,
      int timeout)
  {
    this.adsContext = adsContext;
    this.trustManager = trustManager;
    this.timeout = timeout;
    dn = ConnectionUtils.getBindDN(adsContext.getDirContext());
    pwd = ConnectionUtils.getBindPassword(adsContext.getDirContext());
  }

  /**
   * Reads the configuration of the registered servers.
   * @throws TopologyCacheException if there is an issue reading the
   * configuration of the registered servers.
   */
  public void reloadTopology() throws TopologyCacheException
  {
    suffixes.clear();
    servers.clear();
    try
    {
      Set<Map<ServerProperty,Object>> adsServers =
        adsContext.readServerRegistry();

      Set<ServerLoader> threadSet = new HashSet<ServerLoader>();
      for (Map<ServerProperty,Object> serverProperties : adsServers)
      {
        ServerLoader t = getServerLoader(serverProperties);
        if (isMultiThreaded)
        {
            t.start();
            threadSet.add(t);
        }
        else
        {
            t.run();
        }
      }
      if (isMultiThreaded)
      {
        joinThreadSet(threadSet);
      }
      /* Try to consolidate things (even if the data is not complete). */

      HashMap<LdapName, Set<SuffixDescriptor>> hmSuffixes =
        new HashMap<LdapName, Set<SuffixDescriptor>>();
      for (ServerLoader loader : threadSet)
      {
        ServerDescriptor descriptor = loader.getServerDescriptor();
        for (ReplicaDescriptor replica : descriptor.getReplicas())
        {
          LOG.log(Level.INFO, "Handling replica with dn: "+
              replica.getSuffix().getDN());

          boolean suffixFound = false;
          LdapName dn = new LdapName(replica.getSuffix().getDN());
          Set<SuffixDescriptor> sufs = hmSuffixes.get(dn);
          if (sufs != null)
          {
            Iterator<SuffixDescriptor> it = sufs.iterator();
            while (it.hasNext() && !suffixFound)
            {
              SuffixDescriptor suffix = it.next();
              Iterator<String> it2 = suffix.getReplicationServers().iterator();
              while (it2.hasNext() && !suffixFound)
              {
                if (replica.getReplicationServers().contains(it2.next()))
                {
                  suffixFound = true;
                  Set<ReplicaDescriptor> replicas = suffix.getReplicas();
                  replicas.add(replica);
                  suffix.setReplicas(replicas);
                  replica.setSuffix(suffix);
                }
              }
            }
          }
          if (!suffixFound)
          {
            if (sufs == null)
            {
              sufs = new HashSet<SuffixDescriptor>();
              hmSuffixes.put(dn, sufs);
            }
            sufs.add(replica.getSuffix());
            suffixes.add(replica.getSuffix());
          }
        }
        servers.add(descriptor);
      }

      // Figure out the replication monitoring if it is required.
      if (getFilter().searchMonitoringInformation())
      {
        readReplicationMonitoring();
      }
    }
    catch (ADSContextException ade)
    {
      throw new TopologyCacheException(ade);
    }
    catch (Throwable t)
    {
      throw new TopologyCacheException(TopologyCacheException.Type.BUG, t);
    }
  }

  /**
   * Returns the trust manager used by this class.
   * @return the trust manager used by this class.
   */
  public ApplicationTrustManager getTrustManager()
  {
    return trustManager;
  }

  /**
   * Returns the timeout to establish the connection in milliseconds.
   * @return the timeout to establish the connection in milliseconds. Returns
   * {@code 0} to express no timeout.
   */
  public int getConnectTimeout()
  {
    return timeout;
  }

  /**
   * Reads the replication monitoring.
   * @throws NamingException if an error occurs reading the replication
   * monitoring.
   */
  private void readReplicationMonitoring()
  {
    Set<ReplicaDescriptor> replicasToUpdate = new HashSet<ReplicaDescriptor>();
    for (ServerDescriptor server : getServers())
    {
      for (ReplicaDescriptor replica : server.getReplicas())
      {
        if (replica.isReplicated())
        {
          replicasToUpdate.add(replica);
        }
      }
    }
    for (ServerDescriptor server : getServers())
    {
      if (server.isReplicationServer())
      {
        // If is replication server, then at least we were able to read the
        // configuration, so assume that we might be able to read monitoring
        // (even if an exception occurred before).
        Set<ReplicaDescriptor> candidateReplicas =
          new HashSet<ReplicaDescriptor>();
        // It contains replication information: analyze it.
        String repServer = server.getReplicationServerHostPort();
        for (SuffixDescriptor suffix : getSuffixes())
        {
          Set<String> repServers = suffix.getReplicationServers();
          for (String r : repServers)
          {
            if (r.equalsIgnoreCase(repServer))
            {
              candidateReplicas.addAll(suffix.getReplicas());
              break;
            }
          }
        }
        if (!candidateReplicas.isEmpty())
        {
          Set<ReplicaDescriptor> updatedReplicas =
            new HashSet<ReplicaDescriptor>();
          try
          {
            updateReplicas(server, candidateReplicas, updatedReplicas);
          }
          catch (NamingException ne)
          {
            server.setLastException(new TopologyCacheException(
                TopologyCacheException.Type.GENERIC_READING_SERVER, ne));
          }
          replicasToUpdate.removeAll(updatedReplicas);
        }
      }

      if (replicasToUpdate.isEmpty())
      {
        break;
      }
    }
  }

  /**
   * Sets the list of LDAP URLs and connection type that are preferred to be
   * used to connect to the servers.  When we have a server to which we can
   * connect using a URL on the list we will try to use it.
   * @param cnx the list of preferred connections.
   */
  public void setPreferredConnections(LinkedHashSet<PreferredConnection> cnx)
  {
    preferredConnections.clear();
    preferredConnections.addAll(cnx);
  }

  /**
   * Returns the list of LDAP URLs and connection type that are preferred to be
   * used to connect to the servers.  If a URL is on this list, when we have a
   * server to which we can connect using that URL and the associated connection
   * type we will try to use it.
   * @return the list of preferred connections.
   */
  public LinkedHashSet<PreferredConnection> getPreferredConnections()
  {
    return new LinkedHashSet<PreferredConnection>(preferredConnections);
  }

  /**
   * Returns a Set containing all the servers that are registered in the ADS.
   * @return a Set containing all the servers that are registered in the ADS.
   */
  public Set<ServerDescriptor> getServers()
  {
    HashSet<ServerDescriptor> copy = new HashSet<ServerDescriptor>();
    copy.addAll(servers);
    return copy;
  }

  /**
   * Returns a Set containing the suffixes (replication topologies) that could
   * be retrieved after the last call to reloadTopology.
   * @return a Set containing the suffixes (replication topologies) that could
   * be retrieved after the last call to reloadTopology.
   */
  public Set<SuffixDescriptor> getSuffixes()
  {
    HashSet<SuffixDescriptor> copy = new HashSet<SuffixDescriptor>();
    copy.addAll(suffixes);
    return copy;
  }

  /**
   * Returns the filter to be used when retrieving information.
   * @return the filter to be used when retrieving information.
   */
  public TopologyCacheFilter getFilter()
  {
    return filter;
  }

  /**
   * Method used to wait at most a certain time (MULTITHREAD_TIMEOUT) for the
   * different threads to finish.
   * @param threadSet the list of threads (we assume that they are started)
   * that we must wait for.
   */
  private void joinThreadSet(Set<ServerLoader> threadSet)
  {
    Date startDate = new Date();
    for (ServerLoader t : threadSet)
    {
      long timeToJoin = MULTITHREAD_TIMEOUT - System.currentTimeMillis() +
      startDate.getTime();
      try
      {
        if (timeToJoin > 0)
        {
          t.join(MULTITHREAD_TIMEOUT);
        }
      }
      catch (InterruptedException ie)
      {
        LOG.log(Level.INFO, ie + " caught and ignored", ie);
      }
      if (t.isAlive())
      {
        t.interrupt();
      }
    }
    Date endDate = new Date();
    long workingTime = endDate.getTime() - startDate.getTime();
    LOG.log(Level.INFO, "Loading ended at "+ workingTime + " ms");
  }

  /**
   * Creates a ServerLoader object based on the provided server properties.
   * @param serverProperties the server properties to be used to generate
   * the ServerLoader.
   * @return a ServerLoader object based on the provided server properties.
   */
  private ServerLoader getServerLoader(
      Map<ServerProperty,Object> serverProperties)
  {
    return new ServerLoader(serverProperties, dn, pwd,
        trustManager == null ? null : trustManager.createCopy(),
            timeout,
            getPreferredConnections(), getFilter());
  }

  /**
   * Returns the adsContext used by this TopologyCache.
   * @return the adsContext used by this TopologyCache.
   */
  public ADSContext getAdsContext()
  {
    return adsContext;
  }



  /**
   * Returns a set of error messages encountered in the TopologyCache.
   * @return a set of error messages encountered in the TopologyCache.
   */
  public LinkedHashSet<Message> getErrorMessages()
  {
    Set<TopologyCacheException> exceptions =
      new HashSet<TopologyCacheException>();
    Set<ServerDescriptor> servers = getServers();
    LinkedHashSet<Message> exceptionMsgs = new LinkedHashSet<Message>();
    for (ServerDescriptor server : servers)
    {
      TopologyCacheException e = server.getLastException();
      if (e != null)
      {
        exceptions.add(e);
      }
    }
    /* Check the exceptions and see if we throw them or not. */
    for (TopologyCacheException e : exceptions)
    {
      switch (e.getType())
      {
        case NOT_GLOBAL_ADMINISTRATOR:
          exceptionMsgs.add(INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED.get());

        break;
      case GENERIC_CREATING_CONNECTION:
        if ((e.getCause() != null) &&
            Utils.isCertificateException(e.getCause()))
        {
          exceptionMsgs.add(
              INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER.get(
              e.getHostPort(), e.getCause().getMessage()));
        }
        else
        {
          exceptionMsgs.add(Utils.getMessage(e));
        }
        break;
      default:
        exceptionMsgs.add(Utils.getMessage(e));
      }
    }
    return exceptionMsgs;
  }


  /**
   * Updates the monitoring information of the provided replicas using the
   * information located in cn=monitor of a given replication server.
   * @param replicationServer the replication server.
   * @param candidateReplicas the collection of replicas that must be updated.
   * @param updatedReplicas the collection of replicas that are actually
   * updated.  This list is updated by the method.
   */
  private void updateReplicas(ServerDescriptor replicationServer,
      Collection<ReplicaDescriptor> candidateReplicas,
      Collection<ReplicaDescriptor> updatedReplicas) throws NamingException
  {
    SearchControls ctls = new SearchControls();
    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    ctls.setReturningAttributes(
        new String[] {
            "approx-older-change-not-synchronized-millis", "missing-changes",
            "domain-name", "server-id"
        });
    String filter = "(missing-changes=*)";

    LdapName jndiName = new LdapName("cn=monitor");

    InitialLdapContext ctx = null;
    NamingEnumeration<SearchResult> monitorEntries = null;
    try
    {
      ServerLoader loader =
        getServerLoader(replicationServer.getAdsProperties());
      ctx = loader.createContext();
      monitorEntries = ctx.search(jndiName, filter, ctls);

      while(monitorEntries.hasMore())
      {
        SearchResult sr = monitorEntries.next();

        String dn = ConnectionUtils.getFirstValue(sr, "domain-name");
        int replicaId = -1;
        try
        {
          replicaId =
            new Integer(ConnectionUtils.getFirstValue(sr, "server-id"));
        }
        catch (Throwable t)
        {
          LOG.log(Level.WARNING, "Unexpected error reading replica ID: "+t, t);
        }

        for (ReplicaDescriptor replica: candidateReplicas)
        {
          if (Utils.areDnsEqual(dn, replica.getSuffix().getDN()) &&
              replica.isReplicated() &&
              (replica.getReplicationId() == replicaId))
          {
            try
            {
              replica.setAgeOfOldestMissingChange(
                  new Long(ConnectionUtils.getFirstValue(sr,
                      "approx-older-change-not-synchronized-millis")));
            }
            catch (Throwable t)
            {
              LOG.log(Level.WARNING,
                  "Unexpected error reading age of oldest change: "+t, t);
            }
            try
            {
              replica.setMissingChanges(
                  new Integer(ConnectionUtils.getFirstValue(sr,
                      "missing-changes")));
            }
            catch (Throwable t)
            {
              LOG.log(Level.WARNING,
                  "Unexpected error reading missing changes: "+t, t);
            }
            updatedReplicas.add(replica);
          }
        }
      }
    }
    catch (NameNotFoundException nse)
    {
    }
    finally
    {
      if (monitorEntries != null)
      {
        monitorEntries.close();
      }
      if (ctx != null)
      {
        ctx.close();
      }
    }
  }
}
