/*
 * LdapConnection.java
 *
 * Copyright 2011 John W Dawson
 *
 * This code is distributed under the terms of the GNU General Public License, version 3
 *
 * This class handles the connection to the LDAP server
 */
 
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.ldap.*;
import java.util.*;
import java.util.Hashtable;
import java.net.*;
import java.util.logging.*;
import java.util.regex.*;
public class LdapConnection
{

  private static Logger log = Logger.getLogger ("LdapAddressBook");
  private DirContext ldapContext;
  private Hashtable<String, String> env = new Hashtable<String, String>();
  private boolean connected = false;
  private String ldapServer;
  private JTextField searchField;
  private AddressBookConfiguration configuration;
  private String baseDN;
  private MessageLine messageLine;
  private EditPanel editPanel;
  private long keepAliveInterval = 0;
  private java.util.Timer keepAliveTimer = null;
  private static final SearchControls searchControls = new SearchControls
                         (SearchControls.SUBTREE_SCOPE, 50, 0, null, false, false);
  private static final SearchControls nullControls = new SearchControls
                         (SearchControls.SUBTREE_SCOPE, 1, 0, new String [0], false, false);
  
  public void initialise (JTextField searchField, MessageLine messageLine, 
                          EditPanel editPanel, AddressBookConfiguration configuration)
  {
    // Save ref to configuraration, edit panel and message box
    this.searchField = searchField;
    this.configuration = configuration;
    this.messageLine = messageLine;
    this.editPanel = editPanel;
    
  }
  
  public Boolean connect ()
  {
    
    try
    {    
      // Get base DN from configuration
      baseDN = configuration.getElementValue ("ldapbasedn");
      
      // Set up the environment for creating the initial context for the LDAP connection
      ldapServer = configuration.getElementValue ("ldapserver");
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
      env.put(Context.PROVIDER_URL, "ldap://" + ldapServer + ":" + configuration.getElementValue ("ldapport"));
      
      // Specify SSL if selected
      String securityProtocol = configuration.getElementValue ("ldapsecurityprotocol");
      if (securityProtocol != null)
      {
        env.put(Context.SECURITY_PROTOCOL, securityProtocol);
      }
      
      // Authenticate user
      env.put(Context.SECURITY_AUTHENTICATION, configuration.getElementValue ("ldapsecurityauthentication"));
      env.put(Context.SECURITY_PRINCIPAL, configuration.getElementValue ("ldapsecurityprincipal"));
      env.put(Context.SECURITY_CREDENTIALS, configuration.getEncryptedValue ("ldapsecuritycredentials"));
      
      // Create the initial context
      this.ldapContext = new InitialDirContext(env);
      messageLine.setPersistentText ("Connected to " + ldapServer);
      
      // Get keep alive interval from configuration
      keepAliveInterval = configuration.getIntegerValue ("ldapkeepaliveinterval") * 60000;
    
      // Start the keep alive timer (if specified), to ensure the connection is not closed due to inactivity
      restartKeepAliveTimer (false);
      
      // Enable fields on edit panel if modifications are allowed
      editPanel.enableFields (configuration.getBooleanValue ("allowmodifications"));
      
      // Enable the search box and select it
      searchField.setEnabled (true);
      searchField.requestFocusInWindow ();

      return (connected = true);
      
    }
    catch (CommunicationException e)
    {
      System.out.println (e);
      String cause = e.getCause().getClass().getName();
      if (cause.equals ("java.net.UnknownHostException"))
      {
        JOptionPane.showMessageDialog (null, "The host " + ldapServer + " could not be found", 
                                       "LDAP connection failure", JOptionPane.ERROR_MESSAGE);
      }
      else if (cause.equals ("java.net.SocketException"))
      {
        JOptionPane.showMessageDialog (null, "Unable to connect to host " + ldapServer, 
                                       "LDAP connection failure", JOptionPane.ERROR_MESSAGE);
      }
      else if (cause.equals ("javax.net.ssl.SSLHandshakeException"))
      {
        JOptionPane.showMessageDialog (null, "SSL certificate for LDAP server is not signed by trusted provider", 
                                       "LDAP connection failure", JOptionPane.ERROR_MESSAGE);
      }
      else
      {
        System.out.println (cause);
      }
      messageLine.setPersistentText ("Not connected");
      return (connected = false);     
    }  

    catch (AuthenticationException e)
    {
      JOptionPane.showMessageDialog (null, "The username (bind name) and/or password are not valid", 
                                     "LDAP connection failure", JOptionPane.ERROR_MESSAGE);
      messageLine.setPersistentText ("Not connected");
      return (connected = false);     
    }  

    catch (AuthenticationNotSupportedException e)
    {
      JOptionPane.showMessageDialog (null, "The authentication method is not supported", 
                                     "LDAP connection failure", JOptionPane.ERROR_MESSAGE);
      messageLine.setPersistentText ("Not connected");
      return (connected = false);     
    }  

    catch (Exception e)
    {
      System.out.println (e);
      messageLine.setPersistentText ("Not connected");
      return (connected = false);
    }
           
  }
  
  public NamingEnumeration<SearchResult> search (String filter, boolean retry)
  throws NamingException
  {
    try 
    {
      // Attempt to perform search
      return ldapContext.search (baseDN, filter, searchControls);
    }
    catch (ServiceUnavailableException e)
    {
      // Connection closed due to inactivity
      if (retry)
      {
        // Re-open connection & try one more time
        try 
        {
          ldapContext = new InitialDirContext (env);
        }
        catch (Exception ee)
        {
          messageLine.setTransientText ("Failed to re-open connection to LDAP server");
          log.warning ("LDAP connection failed: " + ee.getMessage());
          return null;
        }          
        log.info ("Connection to LDAP server re-opened");
        return search (filter, false); 
      }
      else
      {
        // We've already tried re-opening the connection, so give up now
        messageLine.setTransientText ("LDAP search failed - check log");
        log.warning ("LDAP search failed: " + e.getMessage());
        return null;
      }
    }
    catch (CommunicationException e)
    {
      // Connection closed due to interruption
      if (retry)
      {
        // Re-open connection & try one more time
        try 
        {
          ldapContext = new InitialDirContext (env);
        }
        catch (Exception ee)
        {
          messageLine.setTransientText ("Failed to re-open connection to LDAP server");
          log.warning ("LDAP connection failed: " + ee.getMessage());
          return null;
        }          
        log.info ("Connection to LDAP server re-opened");
        return search (filter, false); 
      }
      else
      {
        // We've already tried re-opening the connection, so give up now
        messageLine.setTransientText ("LDAP search failed - check log");
        log.warning ("LDAP search failed: " + e.getMessage());
        return null;
      }
    }
  }
 
  private void keepAliveQuery ()
  {
    try
    {
      // Perform a 'null' query to keep connection alive
      ldapContext.search (baseDN, "(cn=*)", nullControls);
      
    }
    catch (NamingException e)
    {
      log.warning ("Keep alive query failed:" + e.getMessage());
    }
      
    // Restart the timer
    restartKeepAliveTimer (false);
  }
  
  public void restartKeepAliveTimer (boolean cancel)
  {
    if (keepAliveInterval > 0)
    {
      // Cancel timer if already running
      if (cancel)
      {
        keepAliveTimer.cancel ();
      }
      
      // Create new timer
      keepAliveTimer = new java.util.Timer ();
      keepAliveTimer.schedule (new TimerTask ()
      {
        public void run ()
        {
          keepAliveQuery ();
        }
      }, keepAliveInterval);   
    }
  }
  
  public Boolean entryExists (String name, Attributes attributes)
  {
    try
    {
      return ldapContext.search (fullDn (name), attributes).hasMore ();
    }
    catch (NamingException e)
    {
      log.warning ("Error searching for entry\n" + e.getMessage());
      return false;
    }
  }
  
  public Boolean createEntry (String name, Attributes attributes)
  {
    try
    {
      ldapContext.createSubcontext (fullDn (name), attributes);
      return true;
    }
    catch (NamingException e)
    {
      log.warning ("Error saving entry\n" + e.getMessage());
      return false;
    }
  }
  
  public Boolean deleteEntry (String name)
  {
    try
    {
      ldapContext.destroySubcontext (fullDn (name));
      return true;
    }
    catch (NamingException e)
    {
      log.warning ("Error deleting entry\n" + e.getMessage());
      return false;
    }
  }
  
  private String fullDn (String name)
  {
    return (name.length() > 0 ? (name + (baseDN.length() > 0 ? ("," + baseDN) : "")) : baseDN);
  }
  
}