/* 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";

    //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 LinkedList    mConnList = new LinkedList();
    private LinkedList    mFreeList = new LinkedList();
    private Object        mPoolLock = new Object();

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

    /**
     * Initialise the OidManager.
     *
     * Initialise the OidManager with the ProviderContext. This method
     * is called immediately after construction and before any other
     * method is called.
     *
     * @param pc the ProviderContext
     */
    public void init(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.

        // Get the OID connection details
        mOidInfo = getOidInfo(pc);

        if ( mOidInfo == null )
        {
            throw new ProviderException("Could not get the OidInfo object");
        }
    }

    /**
     * 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
    {
        DirContext ctx = getContextFromPool((String)null);
        return ctx;
    }

   /**
    * 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 )
       {
           LogManager.severe(
               buildMessage("getUserLDAPContext", 2,
                   userDN, subscriberDN, (String)null, 
                   "has a null argument"));

           return null;
       }

       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 )
        {
            LogManager.severe(
                buildMessage("getUserProperty", 3,
                    userDN, subscriberDN, propertyName, 
                    "has a null argument"));

            return null;
        }

        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 )
        {
            LogManager.severe(
                buildMessage("isUserInGroup", 3,
                    userDN, subscriberDN, groupDN, 
                    "has a null argument"));
            return false;
        }

        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 (UtilException ue)
       {
            LogManager.severe(
                buildMessage("isUserInGroup", 3,
                    userDN, subscriberDN, groupDN, 
                    "failed")
                , ue);
       }
       finally
       {
           releaseLDAPContext(userDir);
       }
       return isMember;
   }

    /**
     * Get the OidInfo class
     *
     * Gets the OidInfo class name from the deployment propertis file  
     * , constructs and initializes the class and returns the object. 
     *
     * @param pc the ProviderContext
     */
    private OidInfo getOidInfo(ProviderContext pc) throws ProviderException
    {
        OidInfo oidInfo = null;
        String oidClassName = (String)
            pc.getAttribute(OidManager.DEPLOYMENT_PARAM_OID_ADMIN_CLASS);

        if ( oidClassName == null )
        {
            throw new ProviderException("No oidAdminClass property not "
                + " found in servicename.properties file");
        }
        else
        {
            oidClassName = oidClassName.trim();

            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, true,
                       Thread.currentThread().getContextClassLoader());
               oidInfo = (OidInfo)oidClass.newInstance();
               oidInfo.init(pc);
           }
           catch (Exception e)
           {
              throw new ProviderException(e);
           }
       }

       return oidInfo;
   }

    /**
     * Creates a new LDAP context from the class derived from OidInfo  
     *
     * @return a LDAP context
     */
    private DirContext createNewContext() 
        throws NamingException, ProviderException
    {
        DirContext ctx;

        if ( mOidInfo == null )
        {
            LogManager.debug("OidManager.createNewContext():"
                             + " Could not get the OidInfo obj");
            throw new ProviderException("Could not get the OidInfo obj");
        }
        else
        {
            ctx = ConnectionUtil.getDefaultDirCtx(
                                 mOidInfo.getLDAPHost(),
                                 mOidInfo.getLDAPPort(),
                                 mOidInfo.getLDAPUser(),
                                 mOidInfo.getLDAPPasswd());
        }
        return ctx;
    }


    /**
     * 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.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 = createNewContext();
            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 1-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;
        }
    }
}
