/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/urm/org/apache/slide/urm/authenticator/userdb/impl/jndi/URMUserDBManagerSpiJNDI.java,v 1.4 2005/03/02 10:53:35 eckehard Exp $
 * $Revision: 1.4 $
 * $Date: 2005/03/02 10:53:35 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.slide.urm.authenticator.userdb.impl.jndi;

import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

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

import org.apache.slide.urm.authenticator.URMAuthenticationFailsException;
import org.apache.slide.urm.authenticator.URMAuthenticatorException;
import org.apache.slide.urm.common.URMCloseConnectionException;
import org.apache.slide.urm.common.URMConfigurationException;
import org.apache.slide.urm.common.URMConfigurator;
import org.apache.slide.urm.common.URMInternalServerException;
import org.apache.slide.urm.utils.messagelogger.MessageLogger;

/**
 * @author zsa
 */
public class URMUserDBManagerSpiJNDI extends URMUserDBManagerSpiJndiBridge
{
    private static org.apache.log4j.Logger msLogger =
        org.apache.log4j.Logger.getLogger(URMUserDBManagerSpiJNDI.class.getName());
        
    //zsa//private DirContext mDirContext = null;
    private String mUsername = null;
    private String mPassword = null;
    
    private ContextCache mContextCache = null;
    
    public URMUserDBManagerSpiJNDI(URMConfigurator userDbConf)
            throws URMConfigurationException, URMAuthenticatorException {
        super(userDbConf);
        mContextCache = new ContextCache(mDirParams.mAuthCacheSize > 30 ? 30 : mDirParams.mAuthCacheSize,
                                         mDirParams.mAuthCacheTime > 30 ? 30 : mDirParams.mAuthCacheTime);
    }
    
    private URMUserDBManagerSpiJNDI(URMUserDBManagerSpiJNDI copyObj, /*DirContext context, */String username, String password) {
        super(copyObj);
        //zsa//mDirContext = context;
        this.mContextCache = copyObj.mContextCache;
        // store it to be able to reconnect later
        mUsername = username;
        mPassword = password;
    }

    private DirContext createDirContext(String user, String password) throws URMAuthenticationFailsException, URMAuthenticatorException, URMCloseConnectionException {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, mDirParams.mProviderUrl);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, mDirParams.mUserTypePropPrefix + user + mDirParams.mUserTypePropPostfix);
        env.put(Context.SECURITY_CREDENTIALS, new String(password));
        try {
            DirContext ctx = new InitialDirContext(env);
            mContextCache.addContext(user, password, ctx);
            return ctx;
        } catch (AuthenticationException ae) {
            throw new URMAuthenticationFailsException(msLogger, "I", ae);
        } catch (NamingException ne) {
            throw new URMAuthenticatorException(msLogger, "E", ne);
        }
    }

    private DirContext getDirContext(String user, String password) throws URMAuthenticationFailsException, URMAuthenticatorException, URMCloseConnectionException {
        if (msLogger.isDebugEnabled())
            msLogger.debug("getDirContext("+user+"): try to create context.");
        DirContext ctx = mContextCache.getContext(user);
        if (ctx == null) {
            ctx = createDirContext(user, password);
            if (msLogger.isDebugEnabled())
                msLogger.debug("getDirContext("+user+"): context is created.");
        }
        else
            if (msLogger.isDebugEnabled())
                msLogger.debug("getDirContext("+user+"): context is read from the cache.");
        return ctx;
    }
    
    private DirContext getDirContextWithAuthentication(String user, String password) throws URMAuthenticationFailsException, URMAuthenticatorException, URMCloseConnectionException {
        if (msLogger.isDebugEnabled())
            msLogger.debug("getDirContextWithAuthentication("+user+"): try to create context.");
        DirContext ctx = mContextCache.getContext(user, password);
        if (ctx == null) {
            ctx = createDirContext(user, password);
            if (msLogger.isDebugEnabled())
                msLogger.debug("getDirContextWithAuthentication("+user+"): context is created.");
        }
        else
            if (msLogger.isDebugEnabled())
                msLogger.debug("getDirContextWithAuthentication("+user+"): context is read from the cache.");
        return ctx;
    }
    
    public URMUserDBManagerSpiJNDI authenticateUser(String user, String password) throws URMAuthenticationFailsException, URMAuthenticatorException, URMCloseConnectionException {
        DirContext ctx = null;
        try {
            ctx = getDirContextWithAuthentication(user, password);
            return new URMUserDBManagerSpiJNDI(this, user, password);
        } finally {
            try {
                mContextCache.freeContext(user, ctx);
            } catch (NamingException e) {
                throw new URMCloseConnectionException(msLogger, "E", e);
            }
        }
    }
    
    /* (non-Javadoc)
     * @see org.apache.slide.urm.authenticator.userdb.URMUserDBManagerSpi#close()
     */
    public void close() throws URMCloseConnectionException {
        /*zsa//try {
            mDirContext.close();
        } catch (NamingException e) {
            throw new URMCloseConnectionException(msLogger, "E", e);
        }*/
    }
    
    private String getFullDn(String name, boolean isUser)
    {
        if (isUser)
            return mDirParams.mUserTypePropPrefix + name + mDirParams.mUserTypePropPostfix;
        else
            return mDirParams.mGroupTypePropPrefix + name + mDirParams.mGroupTypePropPostfix;
    }
    
    private String getNameFromFullDn(String fullDn) {
        int i;
        return fullDn.substring((i = fullDn.indexOf("=") + 1), fullDn.indexOf(",", i)).trim();
    }

    protected Set getGroupsWhereMemberOf(String name, boolean isUser) throws URMInternalServerException
    {
        DirContext dir_ctx = null;
        try {
            dir_ctx = getDirContext(mUsername, mPassword);
            
            String[] attrnames = { mDirParams.mGroupIdField };
            //SearchControls scrls = new SearchControls();
            //scrls.setReturningAttributes(attrnames);
            //scrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            //String filter = "(&(ObjectClass=" + mDirParams.mGroupObjClass[mDirParams.mGroupObjClass.length - 1]
            //                    + ")(" + mDirParams.mMemberAttrOfGroup + "=" + getFullDn(name, isUser) + "))";
            Attributes matchingattrs = new BasicAttributes();
            matchingattrs.put("ObjectClass", mDirParams.mGroupObjClass[mDirParams.mGroupObjClass.length - 1]);
            matchingattrs.put(mDirParams.mMemberAttrOfGroup, getFullDn(name, isUser));
            try {
                NamingEnumeration results = dir_ctx.search(
                        mDirParams.mGroupBaseBindDn, matchingattrs, attrnames);
                //NamingEnumeration results = dir_ctx.search(
                //    isUser ? mDirParams.mPersonBaseBindDn : mDirParams.mGroupBaseBindDn,
                //    filter, scrls);
                
                HashSet set = new HashSet();
                while (results.hasMore()) {
                    SearchResult si = (SearchResult)results.next();
                    Attributes attrs = si.getAttributes();
                    Attribute attr = attrs.get(mDirParams.mGroupIdField);
                    if (attr == null)
                        continue;
                    for (NamingEnumeration ae = attr.getAll(); ae.hasMore(); )
                        set.add((String)ae.next());
                }
                return set;
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        } catch (URMAuthenticationFailsException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMCloseConnectionException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMAuthenticatorException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } finally {
            try {
                mContextCache.freeContext(mUsername, dir_ctx);
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        }
    }

    protected List getGroupMembers(String name, boolean isUser)
            throws URMInternalServerException
    {
        String[] requied_attrs = { mDirParams.mMemberAttrOfGroup };
        DirContext dir_ctx = null;
        try {
            dir_ctx = getDirContext(mUsername, mPassword);
            Attributes attrs = dir_ctx.getAttributes(getFullDn(name, isUser), requied_attrs);
            
            Attribute attr = attrs.get(mDirParams.mMemberAttrOfGroup);
            if (attr == null)
                return null;
            LinkedList list = new LinkedList();
            list.addFirst(null);
            for (NamingEnumeration ae = attr.getAll(); ae.hasMore(); ) {
                String member = (String)ae.next();
                if (member == null || member.length() == 0)
                    continue;
                attrs = dir_ctx.getAttributes(member, new String[]{"ObjectClass"});
                Attribute ocattr = attrs.get("ObjectClass");
                String objclasses[] = new String[ocattr.size()];
                int len = 0;
                for (NamingEnumeration ocae = ocattr.getAll(); ocae.hasMore(); ++len)
                    objclasses[len] = (String)ocae.next();
                int i = mDirParams.mPersonObjClass.length - 1, j;
                for (; i >= 0 ; --i) {
                    for (j = 0; j < len; ++j)
                        if (mDirParams.mPersonObjClass[i].equalsIgnoreCase(objclasses[j]))
                            break;
                    if (j >= len)
                        break;
                }
                if (i < 0)
                    list.addLast(getNameFromFullDn(member));
                else {
                    i = mDirParams.mGroupObjClass.length - 1;
                    for (; i >= 0 ; --i) {
                        for (j = 0; j < len; ++j)
                            if (mDirParams.mGroupObjClass[i].equalsIgnoreCase(objclasses[j]))
                                break;
                        if (j >= len)
                            break;
                    }
                    if (i < 0)
                        list.addFirst(getNameFromFullDn(member));
                    else
                        throw new URMInternalServerException(MessageLogger.getAndLogMessage(
                                                            msLogger, "URMSUE0007", member));
                }
            }
            return list;
        } catch (NamingException e) {
            throw new URMInternalServerException(msLogger, "E", e);
        } catch (URMAuthenticationFailsException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMCloseConnectionException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMAuthenticatorException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } finally {
            try {
                mContextCache.freeContext(mUsername, dir_ctx);
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        }
    }
    
    protected Set getAllEntry(boolean isUser) throws URMInternalServerException {
        DirContext dir_ctx = null;
        try {
            dir_ctx = getDirContext(mUsername, mPassword);
            String filter = "(ObjectClass=", idfield;
            if (isUser) {
                filter += mDirParams.mPersonObjClass[mDirParams.mPersonObjClass.length - 1] + ")";
                idfield = mDirParams.mUserIdField;
            }
            else {
                filter += mDirParams.mGroupObjClass[mDirParams.mGroupObjClass.length - 1] + ")";
                idfield = mDirParams.mGroupIdField;
            }
            String[] attrnames = { idfield };
            SearchControls scrls = new SearchControls();
            scrls.setReturningAttributes(attrnames);
            scrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            try {
                NamingEnumeration results = dir_ctx.search(
                    isUser ? mDirParams.mPersonBaseBindDn : mDirParams.mGroupBaseBindDn,
                    filter, scrls);
                
                HashSet set = new HashSet();
                while (results.hasMore()) {
                    SearchResult si = (SearchResult)results.next();
                    Attributes attrs = si.getAttributes();
                    Attribute attr = attrs.get(idfield);
                    if (attr == null)
                        continue;
                    for (NamingEnumeration ae = attr.getAll(); ae.hasMore(); ) {
                        Object obj = ae.next();
                        if (obj != null)
                            set.add((String)obj);
                    }
                }
                return set;
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        } catch (URMAuthenticationFailsException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMCloseConnectionException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMAuthenticatorException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } finally {
            try {
                mContextCache.freeContext(mUsername, dir_ctx);
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        }
    }

    protected Properties getEntryAttributes(String name, boolean isUser) throws URMInternalServerException
    {
        DirContext dir_ctx = null;
        try {
            dir_ctx = getDirContext(mUsername, mPassword);
            Attributes attrs = dir_ctx.getAttributes(getFullDn(name, isUser),
                isUser ? mDirParams.mAvailableUserAttrNames : mDirParams.mAvailableGroupAttrNames);
            
            Properties props = new Properties();
            String attrval = null;
            Object attrvalobj = null;
            for (NamingEnumeration ae = attrs.getAll(); ae.hasMore(); ) {
                Attribute attr = (Attribute)ae.next();
                if (attr == null || (attrvalobj = attr.get()) == null)
                    continue;
                if (attrvalobj instanceof java.lang.String)
                    attrval = (String)attr.get();
                else if (attrvalobj instanceof byte[])
                    attrval = new String((byte[])attr.get());
                else
                    continue;
                props.setProperty(attr.getID(), attrval);
            }
            return props;
        } catch (NamingException e) {
            throw new URMInternalServerException(msLogger, "E", e);
        } catch (URMAuthenticationFailsException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMCloseConnectionException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } catch (URMAuthenticatorException e1) {
            throw new URMInternalServerException(msLogger, "E", e1);
        } finally {
            try {
                mContextCache.freeContext(mUsername, dir_ctx);
            } catch (NamingException e) {
                throw new URMInternalServerException(msLogger, "E", e);
            }
        }
    }

    /*zsa//protected void finalize() throws URMCloseConnectionException {
        this.close();
    }*/
    
    /**
     * private class to cache information
     */
    private class ContextCache {
        
        private final static String mfsKeyName = "this_would_be_the_key_in_the_cache_if_null_key_parameter_is_received";

        private Hashtable elementCache = new Hashtable();
        private long sizeLimet = 0;
        private long validTime = 0;
        
        protected ContextCache(long sizelimit, long validtime) {
            this.sizeLimet = sizelimit;
            this.validTime  = validtime;
        }
        
        private class CacheElement {
            public String key;
            public String pwd;
            public DirContext context;
            public long   time;
            public long   counter;
            
            public CacheElement(String ckey, String cpwd, DirContext con) {
                this.key     = ckey;
                this.pwd     = cpwd;
                this.context = con;
                this.time    = System.currentTimeMillis();
                this.counter = 0;
            }
        }
        
        protected void freeContext(String key, DirContext context) throws NamingException {
            if (context == null)
                return;
            if (sizeLimet <= 0) {
                context.close();
                return;
            }
            
            String lkey;
            if (key == null) lkey = mfsKeyName;
            else lkey = key;
            CacheElement el = (CacheElement)elementCache.get(lkey);
            if (el != null) {
                synchronized (el) {
                    el.counter--;
                }
            }
            else {
                context.close();
            }
        }
        
        protected synchronized void addContext(String key, String pwd, DirContext context) throws NamingException {
            if (sizeLimet <= 0)
                return;
 
            for (Enumeration en = elementCache.elements(); en.hasMoreElements(); ) {
                CacheElement el = (CacheElement)en.nextElement();
                if (validTime <= (System.currentTimeMillis() - el.time)/1000) {
                    synchronized (el) {
                        if (el.counter == 0) {
                            elementCache.remove(el.key);
                            if (el.context != null) {
                                if (msLogger.isDebugEnabled())
                                    msLogger.debug("cache:addContext("+key+"): closing context '"+el.key+"'.");
                                el.context.close();
                                el.context = null;
                            }
                        }
                    }
                }
            }
            if (elementCache.size() >= sizeLimet)
                return;

            String lkey;
            if (key == null) lkey = mfsKeyName;
            else lkey = key;
            CacheElement el = (CacheElement)elementCache.get(lkey);
            if (el == null) {
                elementCache.put(lkey, new CacheElement(lkey, pwd, context));
            }
        }

        protected DirContext getContext(String key) {
            if (sizeLimet <= 0)
                return null;
                
            String lkey;
            if (key == null) lkey = mfsKeyName;
            else lkey = key;
            CacheElement el = (CacheElement)elementCache.get(lkey);
            if (el != null) {
                synchronized (el) {
                    if (el.context != null) {
                        el.counter++;
                        return el.context;
                    }
                }
            }
            return null;
        }

        protected DirContext getContext(String key, String pwd) {
            if (sizeLimet <= 0)
                return null;
                
            String lkey;
            if (key == null) lkey = mfsKeyName;
            else lkey = key;
            CacheElement el = (CacheElement)elementCache.get(lkey);
            if (el != null) {
                synchronized (el) {
                    if (el.context != null) {
                        if ( (pwd == null && el.pwd != null) || (pwd != null && el.pwd == null) )
                            return null;
                            
                        if ((pwd == null && el.pwd == null) || (pwd.equals(el.pwd))) {
                            el.counter++;
                            return el.context;
                        }
                    }
                }
            }
            return null;
        }

        protected synchronized void clear() throws NamingException {
            int limit = 16;
            while (limit-- > 0) {
                for (Enumeration en = elementCache.elements(); en.hasMoreElements(); ) {
                    CacheElement el = (CacheElement)en.nextElement();
                    if (el != null) {
                        synchronized (el) {
                            if (el.counter == 0) {
                                elementCache.remove(el.key);
                                if (msLogger.isDebugEnabled())
                                    msLogger.debug("cache:clear(): closing context '"+el.key+"'.");
                                el.context.close();
                                el.context = null;
                            }
                        }
                    }
                }
                if (elementCache.size() > 0) {
                    Thread.yield();
                    try { Thread.sleep(10);
                    } catch (InterruptedException e) { }
                }
            }
        }
    }

}
