/*
 * 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.util;

import java.util.LinkedHashSet;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.AuthenticationException;
import javax.naming.NamingException;
import javax.naming.NoPermissionException;
import javax.naming.TimeLimitExceededException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapName;

import org.opends.admin.ads.ADSContext;
import org.opends.admin.ads.ServerDescriptor;
import org.opends.admin.ads.TopologyCacheException;
import org.opends.admin.ads.TopologyCacheFilter;
import org.opends.admin.ads.ADSContext.ServerProperty;

/**
 * Class used to load the configuration of a server.  Basically the code
 * uses some provided properties and authentication information to connect
 * to the server and then generate a ServerDescriptor object based on the
 * read configuration.
 */
public class ServerLoader extends Thread
{
  private Map<ServerProperty,Object> serverProperties;
  private boolean isOver;
  private boolean isInterrupted;
  private String lastLdapUrl;
  private TopologyCacheException lastException;
  private ServerDescriptor serverDescriptor;
  private ApplicationTrustManager trustManager;
  private int timeout;
  private String dn;
  private String pwd;
  private LinkedHashSet<PreferredConnection> preferredLDAPURLs;
  private TopologyCacheFilter filter;

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

  /**
   * Constructor.
   * @param serverProperties the server properties of the server we want to
   * load.
   * @param dn the DN that we must use to bind to the server.
   * @param pwd the password that we must use to bind to the server.
   * @param trustManager the ApplicationTrustManager to be used when we try
   * to connect to the server.
   * @param timeout the timeout to establish the connection in milliseconds.
   * Use {@code 0} to express no timeout.
   * @param preferredLDAPURLs the list of preferred LDAP URLs that we want
   * to use to connect to the server.  They will be used only if they correspond
   * to the URLs that we found in the the server properties.
   * @param filter the topology cache filter to be used.  This can be used not
   * to retrieve all the information.
   */
  public ServerLoader(Map<ServerProperty,Object> serverProperties,
      String dn, String pwd, ApplicationTrustManager trustManager,
      int timeout,
      LinkedHashSet<PreferredConnection> preferredLDAPURLs,
      TopologyCacheFilter filter)
  {
    this.serverProperties = serverProperties;
    this.dn = dn;
    this.pwd = pwd;
    this.trustManager = trustManager;
    this.timeout = timeout;
    this.preferredLDAPURLs =
      new LinkedHashSet<PreferredConnection>(preferredLDAPURLs);
    this.filter = filter;
  }

  /**
   * Returns the ServerDescriptor that could be retrieved.
   * @return the ServerDescriptor that could be retrieved.
   */
  public ServerDescriptor getServerDescriptor()
  {
    if (serverDescriptor == null)
    {
      serverDescriptor = ServerDescriptor.createStandalone(serverProperties);
    }
    serverDescriptor.setLastException(lastException);
    return serverDescriptor;
  }

  /**
   * Returns the last exception that occurred while trying to generate
   * the ServerDescriptor object.
   * @return the last exception that occurred while trying to generate
   * the ServerDescriptor object.
   */
  public TopologyCacheException getLastException()
  {
    return lastException;
  }

  /**
   * {@inheritDoc}
   */
  public void interrupt()
  {
    if (!isOver)
    {
      isInterrupted = true;
      String ldapUrl = getLastLdapUrl();
      if (ldapUrl == null)
      {
        LinkedHashSet<PreferredConnection> urls = getLDAPURLsByPreference();
        if (!urls.isEmpty())
        {
          ldapUrl = urls.iterator().next().getLDAPURL();
        }
      }
      lastException = new TopologyCacheException(
          TopologyCacheException.Type.TIMEOUT,
          new TimeLimitExceededException("Timeout reading server: "+ldapUrl),
          trustManager, ldapUrl);
      LOG.log(Level.WARNING, "Timeout reading server: "+ldapUrl);
    }
    super.interrupt();
  }

  /**
   * The method where we try to generate the ServerDescriptor object.
   */
  public void run()
  {
    lastException = null;
    InitialLdapContext ctx = null;
    try
    {
      ctx = createContext();
      serverDescriptor = ServerDescriptor.createStandalone(ctx, filter);
      serverDescriptor.setAdsProperties(serverProperties);
      serverDescriptor.updateAdsPropertiesWithServerProperties();
    }
    catch (NoPermissionException npe)
    {
      LOG.log(Level.WARNING,
          "Permissions error reading server: "+getLastLdapUrl(), npe);
      if (!isAdministratorDn())
      {
        lastException = new TopologyCacheException(
                TopologyCacheException.Type.NOT_GLOBAL_ADMINISTRATOR, npe,
                trustManager, getLastLdapUrl());
      }
      else
      {
        lastException =
          new TopologyCacheException(
              TopologyCacheException.Type.NO_PERMISSIONS, npe,
              trustManager, getLastLdapUrl());
      }
    }
    catch (AuthenticationException ae)
    {
      LOG.log(Level.WARNING,
          "Authentication exception: "+getLastLdapUrl(), ae);
      if (!isAdministratorDn())
      {
        lastException = new TopologyCacheException(
                TopologyCacheException.Type.NOT_GLOBAL_ADMINISTRATOR, ae,
                trustManager, getLastLdapUrl());
      }
      else
      {
        lastException =
          new TopologyCacheException(
              TopologyCacheException.Type.GENERIC_READING_SERVER, ae,
              trustManager, getLastLdapUrl());
      }
    }
    catch (NamingException ne)
    {
      LOG.log(Level.WARNING,
          "NamingException error reading server: "+getLastLdapUrl(), ne);
      if (ctx == null)
      {
        lastException =
            new TopologyCacheException(
                TopologyCacheException.Type.GENERIC_CREATING_CONNECTION, ne,
                trustManager, getLastLdapUrl());
      }
      else
      {
        lastException =
          new TopologyCacheException(
              TopologyCacheException.Type.GENERIC_READING_SERVER, ne,
              trustManager, getLastLdapUrl());
      }
    }
    catch (Throwable t)
    {
      if (!isInterrupted)
      {
        LOG.log(Level.WARNING,
            "Generic error reading server: "+getLastLdapUrl(), t);
        LOG.log(Level.WARNING, "server Properties: "+serverProperties);
        lastException =
            new TopologyCacheException(TopologyCacheException.Type.BUG, t);
      }
    }
    finally
    {
      isOver = true;
      try
      {
        if (ctx != null)
        {
          ctx.close();
        }
      }
      catch (Throwable t)
      {
      }
    }
  }

  /**
   * Create an InitialLdapContext based in the provide server properties and
   * authentication data provided in the constructor.
   * @return an InitialLdapContext based in the provide server properties and
   * authentication data provided in the constructor.
   * @throws NamingException if an error occurred while creating the
   * InitialLdapContext.
   */
  public InitialLdapContext createContext() throws NamingException
  {
    InitialLdapContext ctx = null;
    if (trustManager != null)
    {
      trustManager.resetLastRefusedItems();

      String host = (String)serverProperties.get(ServerProperty.HOST_NAME);
      trustManager.setHost(host);
    }

    /* Try to connect to the server in a certain order of preference.  If an
     * URL fails, we will try with the others.
     */
    LinkedHashSet<PreferredConnection> conns = getLDAPURLsByPreference();

    for (PreferredConnection connection : conns)
    {
      if (ctx == null)
      {
        lastLdapUrl = connection.getLDAPURL();
        switch (connection.getType())
        {
        case LDAPS:
          ctx = ConnectionUtils.createLdapsContext(lastLdapUrl, dn, pwd,
              timeout, null, trustManager,
              null);
          break;
        case START_TLS:
          ctx = ConnectionUtils.createStartTLSContext(lastLdapUrl, dn, pwd,
              timeout, null, trustManager,
              null, null);
          break;
        default:
          ctx = ConnectionUtils.createLdapContext(lastLdapUrl, dn, pwd,
              timeout, null);
        }
      }
    }
    return ctx;
  }

  /**
   * Returns the last LDAP URL to which we tried to connect.
   * @return the last LDAP URL to which we tried to connect.
   */
  private String getLastLdapUrl()
  {
    return lastLdapUrl;
  }

  /**
   * Returns the non-secure LDAP URL for the given server properties.  It
   * returns NULL if according to the server properties no non-secure LDAP URL
   * can be generated (LDAP disabled or port not defined).
   * @param serverProperties the server properties to be used to generate
   * the non-secure LDAP URL.
   * @return the non-secure LDAP URL for the given server properties.
   */
  private String getLdapUrl(Map<ServerProperty,Object> serverProperties)
  {
    String ldapUrl = null;
    Object v = serverProperties.get(ServerProperty.LDAP_ENABLED);
    boolean ldapEnabled = (v != null) && "true".equalsIgnoreCase(v.toString());
    if (ldapEnabled)
    {
      ldapUrl = "ldap://"+getHostNameForLdapUrl(serverProperties)+":"+
      serverProperties.get(ServerProperty.LDAP_PORT);
    }
    return ldapUrl;
  }

  /**
   * Returns the StartTLS LDAP URL for the given server properties.  It
   * returns NULL if according to the server properties no StartTLS LDAP URL
   * can be generated (StartTLS disabled or port not defined).
   * @param serverProperties the server properties to be used to generate
   * the StartTLS LDAP URL.
   * @return the StartTLS LDAP URL for the given server properties.
   */
  private String getStartTlsLdapUrl(Map<ServerProperty,Object> serverProperties)
  {
    String ldapUrl = null;
    Object v = serverProperties.get(ServerProperty.LDAP_ENABLED);
    boolean ldapEnabled = (v != null) && "true".equalsIgnoreCase(v.toString());
    v = serverProperties.get(ServerProperty.STARTTLS_ENABLED);
    boolean startTLSEnabled = (v != null) &&
    "true".equalsIgnoreCase(v.toString());
    if (ldapEnabled && startTLSEnabled)
    {
      ldapUrl = "ldap://"+getHostNameForLdapUrl(serverProperties)+":"+
      serverProperties.get(ServerProperty.LDAP_PORT);
    }
    return ldapUrl;
  }

  /**
   * Returns the LDAPs URL for the given server properties.  It
   * returns NULL if according to the server properties no LDAPS URL
   * can be generated (LDAPS disabled or port not defined).
   * @param serverProperties the server properties to be used to generate
   * the LDAPS URL.
   * @return the LDAPS URL for the given server properties.
   */
  private String getLdapsUrl(Map<ServerProperty,Object> serverProperties)
  {
    String ldapsUrl = null;
    Object v = serverProperties.get(ServerProperty.LDAPS_ENABLED);
    boolean ldapsEnabled = (v != null) && "true".equalsIgnoreCase(v.toString());
    if (ldapsEnabled)
    {
      ldapsUrl = "ldaps://"+getHostNameForLdapUrl(serverProperties)+":"+
      serverProperties.get(ServerProperty.LDAPS_PORT);
    }
    return ldapsUrl;
  }

  /**
   * Returns the administration connector URL for the given server properties.
   * It returns NULL if according to the server properties no administration
   * connector URL can be generated.
   * @param serverProperties the server properties to be used to generate
   * the administration connector URL.
   * @return the administration connector URL for the given server properties.
   */
  private String getAdminConnectorUrl(
    Map<ServerProperty,Object> serverProperties)
  {
    String adminUrl = null;
    boolean portDefined;

    Object v = serverProperties.get(ServerProperty.ADMIN_ENABLED);
    if ((v != null) && "true".equalsIgnoreCase(String.valueOf(v)))
    {
      v = serverProperties.get(ServerProperty.ADMIN_PORT);
      portDefined = v != null;
    }
    else
    {
      portDefined = false;
    }
    if (portDefined)
    {
      adminUrl = "ldaps://"+getHostNameForLdapUrl(serverProperties)+":"+
      serverProperties.get(ServerProperty.ADMIN_PORT);
    }

    return adminUrl;
  }

  /**
   * Returns the host name to be used to generate an LDAP URL based on the
   * contents of the provided server properties.
   * @param serverProperties the server properties.
   * @return the host name to be used to generate an LDAP URL based on the
   * contents of the provided server properties.
   */
  private String getHostNameForLdapUrl(
      Map<ServerProperty,Object> serverProperties)
  {
    String host = (String)serverProperties.get(ServerProperty.HOST_NAME);
    return ConnectionUtils.getHostNameForLdapUrl(host);
  }

  /**
   * Returns whether the DN provided in the constructor is a Global
   * Administrator DN or not.
   * @return <CODE>true</CODE> if the DN provided in the constructor is a Global
   * Administrator DN and <CODE>false</CODE> otherwise.
   */
  private boolean isAdministratorDn()
  {
    boolean isAdministratorDn = false;
    try
    {
      LdapName theDn = new LdapName(dn);
      LdapName containerDn =
        new LdapName(ADSContext.getAdministratorContainerDN());
      isAdministratorDn = theDn.startsWith(containerDn);
    }
    catch (Throwable t)
    {
      LOG.log(Level.WARNING, "Error parsing authentication DNs.", t);
    }
    return isAdministratorDn;
  }

  /**
   * Returns the list of LDAP URLs that can be used to connect to the server.
   * They are ordered so that the first URL is the preferred URL to be used.
   * @return the list of LDAP URLs that can be used to connect to the server.
   * They are ordered so that the first URL is the preferred URL to be used.
   */
  private LinkedHashSet<PreferredConnection> getLDAPURLsByPreference()
  {
    LinkedHashSet<PreferredConnection> ldapUrls =
      new LinkedHashSet<PreferredConnection>();

    String adminConnectorUrl = getAdminConnectorUrl(serverProperties);
    String ldapsUrl = getLdapsUrl(serverProperties);
    String startTLSUrl = getStartTlsLdapUrl(serverProperties);
    String ldapUrl = getLdapUrl(serverProperties);

    /**
     * Check the preferred connections passed in the constructor.
     */
    for (PreferredConnection connection : preferredLDAPURLs)
    {
      String url = connection.getLDAPURL();
      if (url.equalsIgnoreCase(adminConnectorUrl))
      {
        ldapUrls.add(connection);
      }
      else if (url.equalsIgnoreCase(ldapsUrl) &&
          connection.getType() == PreferredConnection.Type.LDAPS)
      {
        ldapUrls.add(connection);
      }
      else if (url.equalsIgnoreCase(startTLSUrl) &&
          connection.getType() == PreferredConnection.Type.START_TLS)
      {
        ldapUrls.add(connection);
      }
      else if (url.equalsIgnoreCase(ldapUrl) &&
          connection.getType() == PreferredConnection.Type.LDAP)
      {
        ldapUrls.add(connection);
      }
    }

    if (adminConnectorUrl != null)
    {
      ldapUrls.add(
          new PreferredConnection(adminConnectorUrl,
          PreferredConnection.Type.LDAPS));
    }
    if (ldapsUrl != null)
    {
      ldapUrls.add(
          new PreferredConnection(ldapsUrl, PreferredConnection.Type.LDAPS));
    }
    if (startTLSUrl != null)
    {
      ldapUrls.add(new PreferredConnection(startTLSUrl,
              PreferredConnection.Type.START_TLS));
    }
    if (ldapUrl != null)
    {
      ldapUrls.add(new PreferredConnection(ldapUrl,
          PreferredConnection.Type.LDAP));
    }
    return ldapUrls;
  }
}
