/* Copyright (c) 2000, 2001 Oracle Corporation. All Rights Reserved. */
package oracle.portal.provider.v2.oid;

import java.io.UnsupportedEncodingException;

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

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.InitialLdapContext;

import oracle.webdb.utils.SimpleStringBuffer;

import oracle.portal.log.LogManager;

import oracle.portal.provider.v2.ProviderContext;
import oracle.portal.provider.v2.ProviderException;

import oracle.ldap.util.*;
import oracle.ldap.util.jndi.*;

/**
 * An <code>OidManager</code> communicates with the OID server
 * to get details of users and user group membership.
 *
 * This class uses a class derived from OidInfo to get the OID connection
 * details. 
 * This class manages a pool of OID connections to the OID server.
 * It is imperitive that code using this class that gets a connection
 * also calls the release method so the connection can be reused.
 */
public class OidManager
{
    // configuration property names as used in <servicename>.properties
    public static final String DEPLOYMENT_PARAM_OID_MANAGER 
        = "oidManager";
    public static final String DEPLOYMENT_PARAM_OID_ADMIN_CLASS 
        = "oidAdminClass";

    public static final String OID_USER_GIVEN_NAME = "givenName";
    public static final String OID_USER_FAMILY_NAME = "sn";
    public static final String OID_USER_TITLE = "title";
    public static final String OID_USER_INITIALS = "initials";
    public static final String OID_USER_MAIL = "mail";
    public static final String OID_USER_TELEPHONE = "telephoneNumber";
    public static final String OID_USER_DESCRIPTION = "description";
    public static final String OID_USER_DISPLAY_NAME = "displayName";

    // internal static strings
    private static final String OID_SEE_LOG = 
        "  See application log for more details of error";
    private static final String OID_NO_CLASS_NAME = 
        "No oidAdminClass property not found in servicename.properties file";

    //Storage for the connection pool
    //
    // The pool has a number of in use connections mConnList
    // and a number of free connections mFreeList
    // The free list is not allowed to be greater than mFreeSize
    // however the total number of connections is allowed to be greater
    // than mFreeList
    private int mFreeSize = 5; 
    private List mConnList = new LinkedList();
    private LinkedList mFreeList = new LinkedList();
    private Object mPoolLock = new Object();

    // connection details to the OID server
    private OidInfo mOidInfo;

    /**
     * Constructs an OidManager.
     *
     * @param pc the <code>ProviderContext</code>
     */
    public OidManager(ProviderContext pc) throws ProviderException
    {
        // Extract from the provider context the class name of the
        // class that implements OidInfo, create an instance
        // of that class, and use that instance to get details
        // of the OID connection.
        String oidClassName = (String)
            pc.getAttribute(OidManager.DEPLOYMENT_PARAM_OID_ADMIN_CLASS);

        if (oidClassName == null)
        {
            throw new ProviderException(OID_NO_CLASS_NAME);
        }

        try
        {
            // We have to use the the current thread's 
            // 'context class loader'
            // (i.e. the web app's class loader) to load the class, 
            // so that it can be picked up from within the web app
            Class oidClass =
                Class.forName(oidClassName.trim(), true,
                              Thread.currentThread().getContextClassLoader());
            OidInfo oidInfo = (OidInfo)oidClass.newInstance();
            oidInfo.init(pc);
            mOidInfo = oidInfo;
        }
        catch (Exception e)
        {
            throw new ProviderException(e.toString());
        }
    }

    /**
     * Get the application LDAP context.
     *
     * This is the LDAP context for the username/passwd used to connect 
     * to OID. As such it could have a wide scope of capabilities depending
     * on the privileges of the user.
     * When the client code has finished with this connection context 
     * {@link #releaseLDAPContext(DirContext)} must be called.
     *
     * @param pc the ProviderContext
     * @return the directory Context of the logged on user
     */
    protected DirContext getApplicationLDAPContext() 
        throws NamingException, ProviderException
    {
        return getContextFromPool((String)null);
    }

   /**
    * Get the user LDAP context.
    *
    * This is the LDAP context for the user identified by the parameters.
    * What can be done with the context depends on the privileges of the 
    * user.
    * When the client code has finished with this connection context
    * {@link #releaseLDAPContext(DirContext)} must be called.
    *
    * @param userDN the distinguished name of the user
    * @param subscriberDN the distinguished name of the subscriber
    * @return the directory Context of the user
    */
   public DirContext getUserLDAPContext(String userDN, String subscriberDN)
       throws NamingException, ProviderException
   {
       if (userDN==null || subscriberDN==null)
       {
           String msg = buildMessage("getUserLDAPContext", 2,
               userDN, subscriberDN, (String)null, 
               "has a null argument");
           LogManager.severe(msg);
           throw new ProviderException(msg);
       }
       return getContextFromPool(userDN);
   }

   /**
    * Release an LDAP context.
    *
    * Puts the LDAP context back into the pool for reuse by another request.
    *
    * @param ctx The LDAP context to be released.  
    */
   public void releaseLDAPContext(DirContext ctx) throws NamingException
   {
       releaseContextToPool(ctx);
   }

   /**
    * Get a user property.
    *
    * Gets a user property from Oracle Internet Directory.
    * @param userDN the distinguished name of the user
    * @param subscriberDN the distinguished name of the subscriber
    * @param propertyName the name of a user property
    * @return the value of the user property or null if not found
    */
    public String getUserProperty(String userDN, String subscriberDN, 
        String propertyName) throws NamingException, ProviderException 
    {
        if (userDN==null || subscriberDN==null || propertyName==null)
        {
            String msg = buildMessage("getUserProperty", 3,
                userDN, subscriberDN, propertyName, 
                "has a null argument");
            LogManager.severe(msg);
            throw new ProviderException(msg);
        }

        String propertyValue = null;

        // get the Inital directory Context for the proxy user
        DirContext userDir = getUserLDAPContext(userDN, subscriberDN);

        // make sure that the userDir is always closed after use even if
        // an exception is thrown
        try
        {

            User myUser = new User(userDir, Util.IDTYPE_DN, userDN,
                                   Util.IDTYPE_DN, subscriberDN, true);

            // Search OID as the Proxy user for the required property
            PropertySetCollection result = null;
            String userAttrList[] = new String[1];
            userAttrList[0]=propertyName;
            result = myUser.getProperties(userDir,userAttrList);

            // because of the nature of the query the PropertSetCollection will 
            // contain only one property Set
            PropertySet ps = result.getPropertySet(0);

            // The Property Set will contain one Property if the 
            // Property Name in the query matches a Property Name 
            // on the LDAP otherwise there will be no properties
            if (ps.size()==0)
            {
                LogManager.debug(
                    buildMessage("getUserProperty", 3,
                        userDN, subscriberDN, propertyName, 
                        "The requested property does not exist"));
            }
            else
            {
                Property prop = ps.getProperty(0);
                // It is possible for the LDAP to have multiple values for this
                // Property. Get the first one and ignore the rest
                Object obj = prop.getValue(0);
                if (obj instanceof java.lang.String)
                {
                    propertyValue = (String)obj;
                }
                else if (obj instanceof byte[])
                {
                    propertyValue = new String((byte [])obj, "UTF-8");
                }
            }
       }
       catch (UtilException ue)
       {
            LogManager.severe(
                buildMessage("getUserProperty", 3,
                    userDN, subscriberDN, propertyName, 
                    "failed")
                ,ue);
       }
       catch (UnsupportedEncodingException uee)
       {
            LogManager.severe(
                buildMessage("getUserProperty", 3,
                    userDN, subscriberDN, propertyName, 
                    "failed to encode UTF-8")
                ,uee);
       }
       finally
       {
           releaseLDAPContext(userDir);
       }

       return propertyValue;
   }

   /**
    * Test if a user is in a specified group.
    *
    * @param userDN the distinguished name of the user
    * @param subscriberDN the distinguished name of the subscriber
    * @param groupDN the distinguished name of a group
    * @return true if user is a member of the group otherwise false
    */
    public boolean isUserInGroup(String userDN,
                                 String subscriberDN, 
                                 String groupDN)
        throws NamingException, ProviderException
    {
        if (userDN==null || subscriberDN==null || groupDN==null)
        {
            String msg = buildMessage("isUserInGroup", 3,
                userDN, subscriberDN, groupDN, 
                "has a null argument");
            LogManager.severe(msg);
            throw new ProviderException(msg);
        }

        boolean isMember = false;

        // get the Inital directory Context for the proxy user
        DirContext userDir = getUserLDAPContext(userDN, subscriberDN);
        Group myGroup = null;
        User myUser = null;

        // make sure that the userDir is always closed after use even if
        // an exception is thrown
        try
        {
           // Create a LDAP object for the Group
           myGroup = new Group(Util.IDTYPE_DN, groupDN);

           // Create a LDAP object for the User
           myUser = new User(userDir, Util.IDTYPE_DN, userDN,
                             Util.IDTYPE_DN, subscriberDN, false);
           
           // Search OID as the Proxy user for membership of the named group
           // The following line is slow
           isMember = myGroup.isMember(userDir, myUser, true);
        }
        catch (Exception e)
        {
            LogManager.severe(
                buildMessage("isUserInGroup", 3,
                    userDN, subscriberDN, groupDN, 
                    "failed"), e);
            throw new ProviderException(e);
       }
       finally
       {
           releaseLDAPContext(userDir);
       }
       return isMember;
   }

   /**
    * Tests the Oid connection and configuration.
    *
    * Runs some tests to establish if the configuration is set up
    * correctly and the OID can be communicated. 
    *
    * @return a message of any error found or the empty string if ok
    */
    public String test()
    {
        SimpleStringBuffer message = new SimpleStringBuffer(100);

        DirContext ctx = null;
        try
        {
            ctx = getApplicationLDAPContext();
        }
        catch (ProviderException pe)
        {
            String errMsg = pe.getMessage();
            LogManager.severe(
                buildMessage("test", 0, null, null, null, errMsg) ,pe);

            message.append(errMsg).append(OID_SEE_LOG);
        }
        catch (NamingException ne)
        {
            String errMsg = ne.getMessage();
            LogManager.severe(
                buildMessage("test", 0, null, null, null, errMsg) ,ne);

            message.append(errMsg).append(OID_SEE_LOG);
        }
        catch (Exception e)
        {
            String errMsg = e.getMessage();
            LogManager.severe(
                buildMessage("test", 0, null, null, null, errMsg)
                ,e);

            message.append(errMsg).append(OID_SEE_LOG);
        }
        finally
        {
            if (ctx != null)
            {
                try
                {
                    releaseLDAPContext(ctx);
                }
                catch (Exception e)
                {
                    String errMsg = e.getMessage();
                    LogManager.severe(
               	         buildMessage("test", 0, null, null, null, errMsg)
                             ,e);
                }
            }
        }

        return message.toString();
    }

    /**
     * get an LDAP Context from the pool.
     *
     * This method returns the requested context which is either
     * an application context or a proxied user context.
     * This method and releaseContextToPool are the only methods
     * that manipulate the pool of context.
     * This code is syncronised to prevent threads getting the
     * same connection from the pool.
     *
     * @param userDN the users distinguished name. If null the application 
     * context is returned otherwise a proxyed user context is returned.
     * @return a directory Context
     */
    private DirContext getContextFromPool(String userDN)
        throws NamingException, ProviderException
    {
        // This method attempts to manipulate the pool in the syncronized
        // code and defers other actions to be done after the syncronize
        // block.
        //
        // The logic for getting context out of the pool are:-
        // If there is an exact match in the free list to the request use that
        // else if the free list is not full use the oldest existing context
        // else create a new context
        //
        // Note this connection pool never waits and the number off context
        // can grow under heavy demand. However under low load when the context
        // are released the number of context will go down.
        //
        // To reuse a user context for a different user 
        // the context must be first changed to an application context 
        // and then to a proxyed User context. 
        // The needToSetToAppContext means the first action is required
        // and needToReProxy means the secod action is required.
        DirContext ctx = null;
        NameContextPair nameContextPair = null;
        boolean needNewContext = false;
        boolean needToSetToAppContext = false;
        boolean needToReProxy = false;

        synchronized (mPoolLock)
        {
            // First look for a context already connected and proxyed to
            // the user or the application context (userDN==null)
            boolean found=false;
            NameContextPair currNameContextPair=null;
            String currUserDN;
            Iterator iter = mFreeList.iterator();
            while (!found && iter.hasNext())
            {  
                currNameContextPair = (NameContextPair)iter.next();
                currUserDN = currNameContextPair.getName();
                if ((userDN==null && currUserDN==null)
                    || (currUserDN!=null && currUserDN.equals(userDN)))
                {
                    found = true;
                }
            }  
    
            if (found)
            {
                // A context that matches the request has been found
                // Move the context from the free list to the used list
                nameContextPair = currNameContextPair;
                mFreeList.remove(nameContextPair);
                mConnList.add(nameContextPair);
                ctx = nameContextPair.getContext();
            }
            else if (mFreeList.size() > 0)
            {
                // There is no context in the free list that matches 
                // the request. However there is at least one context
                // in the free list. Use the oldest context and put 
                // into the used list.
                nameContextPair = (NameContextPair)mFreeList.removeFirst();
                mConnList.add(nameContextPair);
                ctx = nameContextPair.getContext();
                if (userDN == null)
                {
                    // since the request for an application context was 
                    // not found, this context is currently a user Context
                    needToSetToAppContext = true;
                }
                else
                {
                    // since the request for a user context was not found,
                    // this context is currently an application context
                    // or a user context for a different user
                    needToSetToAppContext = (nameContextPair.getName()!=null);
                    needToReProxy = true;
                }
            } 
            else   // mFreeList.size() ==  0 
            {
                // There is no free context in the free list.
                // So create a new context.
                // Add the new context to the used List.
                needNewContext = true;
                nameContextPair = new NameContextPair();
                mConnList.add(nameContextPair);
                needToReProxy = (userDN!=null);
            }
        }  // end syncronize
       
        // do the defered actions outside the syncronization block.
        // These actions alter the contents of an entry in the mConnList 
        if (needNewContext)
        {
            // create a brand new application context and store 
            ctx = ConnectionUtil.getDefaultDirCtx(
                mOidInfo.getLDAPHost(),
                mOidInfo.getLDAPPort(),
                mOidInfo.getLDAPUser(),
                mOidInfo.getLDAPPasswd());

            nameContextPair.setName(null);
            nameContextPair.setContext(ctx);
        }
        if (needToSetToAppContext)
        {
            // this context needs to be re-bind to the application context
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, 
                mOidInfo.getLDAPUser());
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, 
                mOidInfo.getLDAPPasswd());
            ((LdapContext)ctx).reconnect(null);
            nameContextPair.setName(null); // null means an application context
        }
        if (needToReProxy)
        {
            // The context needs to be proxied to a user
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, "");
            ((LdapContext)ctx).reconnect(null);
            nameContextPair.setName(userDN);
        }
        return ctx;
    }

    /**
     * Release an LDAP Context back to the pool.
     *
     * Puts an LDAP context back into the pool.
     * This code is syncronised to prevent to threads getting the
     * same connection from the pool.
     *
     * @param ctx the DirContext to be released
     */
    private void releaseContextToPool(DirContext ctx) throws NamingException
    {
        DirContext ctxToBeClosed = null;
        boolean found = false;
        NameContextPair nameContextPair = null;
        synchronized(mPoolLock)
        {
            Iterator iter = mConnList.iterator();
            while (!found && iter.hasNext())
            {  
                nameContextPair = (NameContextPair)iter.next();
                if (nameContextPair.getContext() == ctx)
                {
                    found = true;
                }
            }  
            if (found)
            {  
                  // move the context from the used to the free list
                  // if there is space in the free list
                  mConnList.remove(nameContextPair);
                  if (mFreeList.size() < mFreeSize)
                  {
                      mFreeList.add(nameContextPair);
                  }
                  else
                  {
                      ctxToBeClosed = nameContextPair.getContext();
                  }
            }
            else
            {
                // we ignore this context as we did not create it.
                LogManager.debug("OidManager.releaseContextToPool(): "
                    + "Requested to release a context we did not create!");
            }
 
        }   // end synchronized
        
        // Do the defered actions outside the synchronized block
        if (ctxToBeClosed != null)
        {
            try
            {
                // Dont wait for the garbage collector to close the context.
                // Force a close here to free up resources on the OID server.
                // just being paranoid, since after the close() no
                // other calls on the context are allowed.
                ctxToBeClosed.close(); ctxToBeClosed = null; 
            }
            catch(Exception e)
            {
                LogManager.severe("OidManager.releaseContextToPool():"
                                      + " Exception closing context", e);
            }
        }
    }

    /**
     * Log the connection pool state.
     *
     * Puts an LDAP context back into the pool.
     * This code is syncronised to prevent to threads getting the
     * same connection from the pool.
     *
     * @param pre text to be printed before the connection pool state
     */
    private void logState(String pre)
    {
        synchronized(mPoolLock)
        {
            LogManager.debug(pre + " FreeList=" +  mFreeList.toString() 
                                 + " UsedList=" +  mConnList.toString());
        }
    }

    /**
     * Build a method call message string
     *
     * Builds a message of the method name, arguments and
     * message text to be used for the log manager.
     *
     * @param name name of the method
     * @param noParams the number of parameters values 0-3 are legal
     * @param param1 the value of the first parameter as a string or a null
     * @param param2 the value of the second parameter as a string or a null
     * @param param3 the value of the third parameter as a string or a null
     * @param message the message text
     */
    private String buildMessage(String name, int noParams, String param1,
        String param2, String param3, String msg)
    {
        SimpleStringBuffer buf = new SimpleStringBuffer(200);
        buf.append("OidManager.")
            .append(name)
            .append("(\"");
        if (noParams > 0) 
        {
            buf.append(String.valueOf(param1)); 
        }
        if (noParams > 1) 
        {
            buf.append("\", \"")
                .append(String.valueOf(param2)); 
        }
        if (noParams > 2) 
        {
            buf.append("\", \"")
                .append(String.valueOf(param3)); 
        }
        buf.append("\") ")
            .append(msg);
        return buf.toString();
    }

    private static class NameContextPair
    {
        String mName;
        DirContext mCtx;

        public String getName()
        {
            return mName;
        }

        public DirContext getContext()
        {
            return mCtx;
        }

        public void setName(String name)
        {
            mName = name;
        }

        public void setContext(DirContext ctx)
        {
            mCtx = ctx;
        }

        public boolean isApplicationContext()
        {
            return (mName==null);
        }

        public String toString()
        {
            String str = (mName==null)?"null":mName;
            str += "/" + ((mCtx==null)?"null": mCtx.toString());
            return str;
        }
    }

}
