package org.esupportail.cas.server.handlers.ldap;

import java.util.Iterator;
import java.util.LinkedList;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.dom4j.Element;
import org.esupportail.cas.server.util.RedundantHandler;

/**
 * <p>This class implements an LDAP server class.</p>
 * <p>It can authenticate users by:</p>
 * <ol>
 * <li>searching into the LDAP directory to guess the user's
 *      DN thanks to its username;</li>
 * <li>binding to the same LDAP directory with the DN found 
 *      previously and the password provided by the user.</li>
 * </ol>
 * <p>It is used by BindLdapHandler.</p>
 *
 * @author Pascal Aubry <pascal.aubry at univ-rennes1.fr>
 * @author Jean-Baptiste Daniel <danielj at sourceforge.net>
 */
public final class BindLdapServer extends LdapServer {
	
	/**
	 * Constructor.
	 *
	 * @param handlerDebug debugging mode of the handler
	 * @param handler      the handler the server will be used by
	 * @param serverElement the XML element that declares the server 
	 * @throws Exception Exception
	 */
	public BindLdapServer(
			final Boolean handlerDebug,
			final RedundantHandler handler,
			final Element serverElement) throws Exception {
		super(handlerDebug, handler, serverElement);
		traceBegin();
		traceEnd();
	}
	
	/**
	 * Try to authenticate a user (by searching and then binding to 
	 * the LDAP directory).
	 *
	 * @param username the user's name
	 * @param password the user's password
	 *
	 * @return Server.AUTHENTICATE_SUCCESS, Server.AUTHENTICATE_NOAUTH
	 * or Server.AUTHENTICATE_FAILURE.
	 */	
	public int authenticate(final String username,
			final String password) {
		traceBegin();
		
		// retrieve the handler we are working for
		BindLdapHandler handler = (BindLdapHandler) getHandler();
		
		// connect as a privileged user
		DirContext searchContext = connect(handler.getBindDn(), handler.getBindPassword());
		if (getConnectError() != CONNECT_SUCCESS) {
			trace("Could not bind as a privileged user.");
			if (searchContext != null) {
				try {
					trace("Closing LDAP connection...");
					searchContext.close();
				} catch (NamingException e) {
					trace("Could not close connection.");
				}
			}
			traceEnd("AUTHENTICATE_FAILURE");
			return AUTHENTICATE_FAILURE;
		}
		
		// Initialize search constraints parameters
		
		// 1. search scope to OBJECT_SCOPE (0), ONELEVEL_SCOPE (1), or
		//    SUBTREE_SCOPE (2).
		String scope = handler.getScope();
		int scopeValue;
		if (scope.equals("base")) {
			scopeValue = 0;
		} else if (scope.equals("one")) {
			scopeValue = 1;
		} else {
			scopeValue = 2;
		}
		
		// Attributes to return, null -> all; "" -> nothing
		final String[] returnedAttributes = { 
				"dn" 
		};
		
		// create a new searchControl object with the parameters we have set
		trace("Creating search constraints...");
		SearchControls constraints = new SearchControls(
				scopeValue,
				// Maximum number to return, 0 -> no limit
				1000, 
				// Number of ms to wait before return, 0 -> infinite
				1000,
				// attributes to return
				returnedAttributes,
				// return the object bound to the name
				false,
				// deference the link during search
				false);
		
		// search in the LDAP directory
		constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
		try {
			trace("Searching in the LDAP directory...");
			NamingEnumeration results = searchContext.search(handler.getSearchBase(),
					replaceTokens(handler.getFilter(), username),
					constraints);
			// for each result, store the dn in an array
			LinkedList dnList = new LinkedList();
			try {
				while (results != null && results.hasMore()) {
					// get the resulting DN from the results
					SearchResult searchResult = (SearchResult) results.next();
					String name= searchResult.getName();
					String dnName= name + ',' + handler.getSearchBase();
					dnList.add( dnName );
				}
			} catch(javax.naming.PartialResultException pre) {
				trace("Warning: (" + pre.getClass().getName() + "): " + pre.getMessage());
			}
			trace("Closing LDAP connection...");
			searchContext.close();
			searchContext = null;
			
			// count the number of results
			if (dnList.size() == 0) {
				trace("Username not found.");
			} else {
				if (!handler.areMultipleAccountsEnabled() && (dnList.size() > 1)) {
					trace("Multiple accounts are not allowed (use <enable_multiple_accounts>).");
				} else {
					for (Iterator i = dnList.iterator(); i.hasNext();) {
						String dn = (String) i.next();

						// try to bind to the LDAP directory
						trace("try to bind as DN '" + dn + "'...");
						connectAndClose(dn, password);
						
						switch (getConnectError()) {
							case CONNECT_SUCCESS:
								trace("Bind succeeded.");
								traceEnd("AUTHENTICATE_SUCCESS");
								return AUTHENTICATE_SUCCESS;
							case CONNECT_NOAUTH:
								// could not connect, nothing to be done
								break;
							default:
								trace("Bind failed.");
							traceEnd("AUTHENTICATE_FAILURE");
							return AUTHENTICATE_FAILURE;
						}
					}
				}
			}
		} catch (Exception e) {
			trace("Failure (" + e.getClass().getName() + "): " + e.getMessage());
		}
		
		if (searchContext != null) {
			try {
				searchContext.close();
			} catch (NamingException e) {
				trace("Could not close connection.");
			}
		}

		traceEnd("AUTHENTICATE_NOAUTH");
		return AUTHENTICATE_NOAUTH;
	}
	
}