/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.security;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import gov.va.med.fw.model.UserPrincipalImpl;
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.service.MultipleRecordsFoundException;
/**
 * Generic LDAP interface for searching entries in Windows Directory server
 * @author Madhu Katikala
 *
 */
public class LDAPServiceImpl extends AbstractComponent implements LDAPService {

    public static final String LDAP_ATTR_USER_PRINCIPAL_NAME = "userPrincipalName";
    public static final String LDAP_ATTR_USER_ACCOUNT_NAME = "sAMAccountName";
    public static final String LDAP_ATTR_USER_FIRST_NAME = "givenName";
    public static final String LDAP_ATTR_USER_LAST_NAME = "sn";
    public static final String LDAP_ATTR_USER_JOB_TITLE = "title";
    public static final String LDAP_ATTR_USER_MIDDLE_INITIALS = "initials";
    public static final String LDAP_DISTINGUISHED_NAME = "distinguishedName";
    
    private String domainContext = "DC=vha,DC=med,DC=va,DC=gov";
    private String ldapInitialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
    private String authentication = "simple";
    private String securityRealm = null;
    private String ldapSASLqopMode = null;
    private String adminUserId = null;
    private String adminPassword = null;
    private List ldapServers = new ArrayList ();
    private int timeOutMilliSeconds = 5000;
        
    public LDAPServiceImpl() {
        super();   
    }
    
    /**
     * Login to the LDAP server with the specified user name and password
     * and return User info if sucessful
     */
    public UserPrincipal authenticate(String userName, String password)
            throws ServiceException {
    	
        UserPrincipal user = null;
        
        //Get LDAP Connection
        DirContext connection = getConnection(userName, password);
        
        //Get User Attributes
        try {
        	user = getUserInfo(connection,userName,null,null,null);
        }finally {
        	closeConnection(connection);
        }
        return user;
    }

    /**
     * Connecto to LDAP server with admin id and password and 
     * retrive user information    
     */
    public UserPrincipal getUserInfo(String userName) throws ServiceException {
        //Get LDAP Connection
        DirContext connection = getConnection();

        //Get User Attributes
        UserPrincipal user = null;
        
        //Get User Attributes
        try {
        	user = getUserInfo(connection,userName,null,null,null);
        } finally {
        	closeConnection(connection);
        }
        return user;        
    }

    /**
     * Search LDAP with first, last and middle name and return user info
     */
    public UserPrincipal getUserInfo(String firstName, String lastName,
            String middleName) throws ServiceException {
        //Get LDAP Connection
        DirContext connection = getConnection();

        //Get User Attributes
        UserPrincipal user = null;
        
        //Get User Attributes
        try {
        	user = getUserInfo(connection,null,firstName,lastName,middleName);
        } finally {
        	closeConnection(connection);
        }
        return user;        
    }
    
    public String getAuthentication() {
        return authentication;
    }

    public void setAuthentication(String authentication) {
        this.authentication = authentication;
    }

    public String getDomainContext() {
        return domainContext;
    }

    public void setDomainContext(String domainContext) {
        this.domainContext = domainContext;
    }

    public String getLdapInitialContextFactory() {
        return ldapInitialContextFactory;
    }

    public void setLdapInitialContextFactory(String ldapInitialContextFactory) {
        this.ldapInitialContextFactory = ldapInitialContextFactory;
    }

    public String getLdapSASLqopMode() {
        return ldapSASLqopMode;
    }

    public void setLdapSASLqopMode(String ldapSASLqopMode) {
        this.ldapSASLqopMode = ldapSASLqopMode;
    }

    public String getSecurityRealm() {
        return securityRealm;
    }

    public void setSecurityRealm(String securityRealm) {
        this.securityRealm = securityRealm;
    }

    public String getAdminPassword() {
        return adminPassword;
    }

    public void setAdminPassword(String adminPassword) {
        this.adminPassword = adminPassword;
    }

    public String getAdminUserId() {
        return adminUserId;
    }

    public void setAdminUserId(String adminUserId) {
        this.adminUserId = adminUserId;
    }
    
    public List getLdapServers() {
		return this.ldapServers;
	}

	public void setLdapServers(List ldapServers) {
		if (ldapServers != null && ldapServers.size() > 0) {		
			this.ldapServers = ldapServers;
		}
	}

	public int getTimeOutMilliSeconds() {
		return this.timeOutMilliSeconds;
	}

	public void setTimeOutMilliSeconds(int timeOutMilliSeconds) {
		if ( timeOutMilliSeconds > 0)
			this.timeOutMilliSeconds = timeOutMilliSeconds;
	}

	/**
     * Search LDAP with specified search criteria - Generic Implemenations
     * @return
     * @throws ServiceException
     */
    
    private UserPrincipal getUserInfo(DirContext connection,
            String userName, String firstName, 
            String lastName, String middleName) throws ServiceException {
 
        UserPrincipal user = null;
        
        try {
            //Build search control with the required attributes
            SearchControls searchCtls = 
                new SearchControls(SearchControls.SUBTREE_SCOPE, 0, getTimeOutMilliSeconds(),
                    new String[] {  
                        LDAP_ATTR_USER_PRINCIPAL_NAME, 
                        LDAP_ATTR_USER_ACCOUNT_NAME,
                        LDAP_ATTR_USER_FIRST_NAME,
                        LDAP_ATTR_USER_LAST_NAME,
                        LDAP_ATTR_USER_JOB_TITLE,
                        LDAP_ATTR_USER_MIDDLE_INITIALS,
                        LDAP_DISTINGUISHED_NAME},
                    false, false);
        
            //search filter
            StringBuffer searchFilter = new StringBuffer("(&(objectclass=User)");
            if (userName != null) {
            	String filteredUserName = escapeDN(userName);
            	
                //use the user principal filter if the name is qualified domain user
                if (userName.indexOf('@') > 0) {
                    searchFilter.append("(" + LDAP_ATTR_USER_PRINCIPAL_NAME + "=" + filteredUserName + ")");
                }
                //use the old windows network id format
                 else {
                     searchFilter.append("(" + LDAP_ATTR_USER_ACCOUNT_NAME + "=" + filteredUserName + ")"); 
                }
            }
   
            if (firstName != null) {
                searchFilter.append("(" + LDAP_ATTR_USER_FIRST_NAME + "=" + escapeDN(firstName) + ")");
            }
            if (lastName != null) {
                searchFilter.append("(sn=" + LDAP_ATTR_USER_LAST_NAME + ")");
            }
            if (middleName != null) {
                searchFilter.append("(initials=" + LDAP_ATTR_USER_MIDDLE_INITIALS + ")");
            }
        
            searchFilter.append(")");
    
            //query ldap    (search for objects)
            NamingEnumeration results = 
                connection.search(getDomainContext(), searchFilter.toString(),searchCtls);
                    
            //process the result set
            if (results.hasMoreElements()) {
                //retrieve attributes and build user object
                SearchResult sr = (SearchResult) results.next();
                if (results.hasMoreElements()) {
                    throw new MultipleRecordsFoundException("Multiple Records found");
                }
   
                Attributes attrs = sr.getAttributes();  
                
                if (attrs != null) {                    
                    Map properties = new HashMap ();
                    for (NamingEnumeration ae = attrs.getAll(); ae.hasMore(); )
                    {
                        Attribute attr = (Attribute) ae.next();

                        String attributeName = attr.getID();

                        //only the first element is retrieved  
                        for (NamingEnumeration e = attr.getAll(); e.hasMoreElements();)
                        {
                            properties.put(attributeName,e.nextElement());
                            break;
                        }
                    }
                    user = buildUserPrincipal(properties);
                }    
            }
        }
        catch (ServiceException se) {
            throw se;
        }
        catch (Exception ex) {
            throw new ServiceException("LDAP Query failed", ex);
        }
        return user;
    }
    
    /**
     * Escape out certain meta-characters from the distinguished name (DN) passed to LDAP query
     * @param distinguished name (DN)
     * @return filtered distinguished name (DN)
     */    
    private static String escapeDN(String name) {
        StringBuffer sb = new StringBuffer();
        if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
            sb.append('\\'); // add the leading backslash if needed
        }
        for (int i = 0; i < name.length(); i++) {
            char curChar = name.charAt(i);
            switch (curChar) {
                case '\\':
                    sb.append("\\\\");
                    break;
                case ',':
                    sb.append("\\,");
                    break;
                case '+':
                    sb.append("\\+");
                    break;
                case '"':
                    sb.append("\\\"");
                    break;
                case '<':
                    sb.append("\\<");
                    break;
                case '>':
                    sb.append("\\>");
                    break;
                case ';':
                    sb.append("\\;");
                    break;
                default:
                    sb.append(curChar);
            }
        }
        if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
            sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
        }
        return sb.toString();
    }

    /**
     * Build UserPrincipal object from LDAP results
     * @param properties
     * @return
     */
    protected UserPrincipal buildUserPrincipal(Map properties) {
        String userName = (String) properties.get(LDAP_ATTR_USER_PRINCIPAL_NAME);
        userName = userName.substring(0,userName.indexOf('@'));
        UserPrincipal user = new UserPrincipalImpl(userName);                      
        return user;
    }
    /**
     * Close LDAP Connection
     * @param connection
     * @throws ServiceException
     */
    private void closeConnection(DirContext connection){
        try {
            connection.close();
        }catch (Exception ex) {
            logger.error("LDAP Connection could not be closed",ex);
        }
    }
    /**
     * Get LDAP connection for user verification using admin credentials
     * @return
     * @throws ServiceException
     */
    private DirContext getConnection() throws ServiceException {
    	return getConnection(getAdminUserId(), getAdminPassword());
    }
    
    /**
     * Get LDAP connection for authentication using user credentials
     * @return
     * @throws ServiceException
     */    
    private DirContext getConnection(String userName, String password) 
    throws ServiceException {
    	
    	List servers = getLdapServers();
    	ServiceException prevEx = null;
    	for (int i=0; i<servers.size(); i++){
    		try {
    			DirContext context = 
    				getConnection((String)servers.get(i), userName, password);
    			//if the connection is successful and it is not the first one make it primary
    			if (i > 0) {
    				String newPrimary = (String)servers.get(i);
    				String oldPrimary = (String)servers.get(0);
    				servers.set(0,newPrimary);
    				servers.set(i,oldPrimary);
    				setLdapServers(servers);
    			}
    			return context;
    		}catch (ServiceException e){
    			prevEx = e;
    		}
    		logger.error("LDAP Connection attempt with server " + (String)servers.get(i) + " Failed");
    	}
    	//If the connection is not successful throw Service Exception
    	if (prevEx == null) 
    		throw new ServiceException("LADP Connextion failed");
    	else throw prevEx;    	    	
    }	
    /**
     * Get LDAP Connection
     * @param userName
     * @param password
     * @return
     * @throws ServiceException
     */
    private DirContext getConnection(String ldapServer, String userName, String password) 
        throws ServiceException {
        try {
            //Set environment variables for LDAP Connection
            Hashtable env = new Hashtable(10);
            env.put(Context.PROVIDER_URL,ldapServer); 
            env.put(Context.INITIAL_CONTEXT_FACTORY,getLdapInitialContextFactory());
            env.put(Context.SECURITY_AUTHENTICATION,getAuthentication());
            env.put(Context.SECURITY_PRINCIPAL, userName);
            
            //convert password to bytes (encode with UTF-8
            byte[] pwdBytes = password.getBytes("UTF-8");
            env.put(Context.SECURITY_CREDENTIALS, pwdBytes);
            
            if (getSecurityRealm() != null) {
                env.put("java.naming.security.sasl.realm", getSecurityRealm());
            }
    
            // LDAP SASL qop (Optional)
            if (getLdapSASLqopMode() != null) {
                env.put("javax.security.sasl.qop", getLdapSASLqopMode());
            }
            
            DirContext connection = new InitialDirContext(env);
            return connection;
        }
        catch (Exception ex) {
            throw new ServiceException("LDAP Connection Failed", ex);
        }
    }
}
