/*
 * $Header: /home/cvspublic/jakarta-slide/wck/src/org/apache/slide/simple/authentication/AbstractPoolingConnectionManager.java,v 1.1 2004/12/09 12:17:09 ozeigermann Exp $
 * $Revision: 1.1 $
 * $Date: 2004/12/09 12:17:09 $
 *
 * ====================================================================
 *
 * Copyright 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.simple.authentication;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

/**
 * Framework for a pooling connection manager. Session objects as returned with
 * the {@link SessionAuthenticationManager} are supposed to be somewhat
 * expensive to create to justify reusing them in a pool of this kind. We call
 * such sessions a connection, that's where the name comes from.
 * 
 * <p>
 * This might be a bit less elegant as other similar solutions, but requires you
 * to implement five obvious methods only.
 * </p>
 * <p>
 * Commons pooling with the {@link GenericObjectPool}is used. Note that for
 * each user/password combination a dedicated pool is maintained.
 * </p>
 * <p>
 * When inheriting from this class you might also adapt the default settings to
 * your needs. Note that the default pool size of 2 makes more sense than
 * apparent as <em>a single user</em> is thus allowed to have two concurrent
 * sessions.
 * </p>
 *  
 */
public abstract class AbstractPoolingConnectionManager implements SessionAuthenticationManager {

    // name:password -> Pool
    private Map namePassword2PoolMap = new HashMap();

    // connection -> Pool
    private Map session2PoolMap = new HashMap();

    // name -> name:password
    private Map fakeMap = new HashMap();

    protected int maxActive;

    protected byte whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_BLOCK;

    protected long maxWait;

    protected boolean testOnBorrow;

    protected boolean testOnReturn;

    /**
     * Creates a new pooling manager with 2 possible connections per user, a
     * timeout of 10 seconds and a validaty check of each connection when it
     * gets fetched from the pool.
     *  
     */
    public AbstractPoolingConnectionManager() {
        this(2, 10000, true, false);
    }

    /**
     * Detailed constructor which can be called by subclasses to match it your
     * needs.
     * 
     * @param maxActive
     *            maximum number of open connections per user
     * @param maxWait
     *            maximum time to wait for a connection im milliseconds
     * @param testOnBorrow
     *            <code>true</code> if a connection shall be checked for
     *            validity before getting it from the pool
     * @param testOnReturn
     *            <code>true</code> if a connection shall be checked for
     *            validity before returning it from the pool
     */
    public AbstractPoolingConnectionManager(int maxActive, long maxWait, boolean testOnBorrow, boolean testOnReturn) {
        this.maxActive = maxActive;
        this.maxWait = maxWait;
        this.testOnBorrow = testOnBorrow;
        this.testOnReturn = testOnReturn;
    }

    /**
     * Tries to authenticate a user to the custom store and return a connection.
     * In case there is no connection available this request will block until
     * either one becomes available or a timeout occurs.
     * 
     * @param user
     *            the name of the user who is requesting an authenticated
     *            session
     * @param password
     *            the password of the user who is requesting an authenticated
     *            session
     * @return either a newly created connection or a free one from the pool
     * @throws Exception
     *             in case authentication to your custom store failed or
     *             anything else went wrong
     */
    public Object getAuthenticationSession(String user, String password) throws Exception {
        ObjectPool pool = getPool(user, password);
        Object conn = pool.borrowObject();
        // make sure association between connection and pool exists for later
        // closing it properly
        session2PoolMap.put(conn, pool);
        fakeMap.put(user, password);
        return conn;
    }

    /**
     * Tries to authenticate a user to the custom store and return a connection.
     * No password is supplied as this is a trusted user who has proven its
     * identity to JAAS authentication before. In case there is no connection
     * available this request will block until either one becomes available or a
     * timeout occurs.
     * 
     * @param user
     *            the name of the user who is requesting an authenticated
     *            session
     * @return either a newly created connection or a free one from the pool
     * @throws Exception
     *             in case authentication to your custom store failed or
     *             anything else went wrong
     */
    public Object getAuthenticationSession(String user) throws Exception {
        String password = (String) fakeMap.get(user);
        if (password == null) {
            // not authenticated
            return null;
        }
        Object conn = getAuthenticationSession(user, password);
        return conn;
    }

    /**
     * Returns a connection to the pool. Usually this does not close the
     * session.
     * 
     * @param session
     *            the connection to be closed
     * @throws Exception
     *             whent the session is unknown
     */
    public void closeAuthenticationSession(Object session) throws Exception {
        ObjectPool pool = (ObjectPool) session2PoolMap.get(session);
        if (pool != null) {
            pool.returnObject(session);
        } else {
            throw new Exception("Session " + session + " is unkown!");
        }
    }

    protected synchronized ObjectPool getPool(String user, String password) {
        String key = user + ":" + password;
        ObjectPool pool = (ObjectPool) namePassword2PoolMap.get(key);
        if (pool == null) {
            pool = createNewPool(user, password);
            namePassword2PoolMap.put(key, pool);
        }
        return pool;
    }

    protected ObjectPool createNewPool(String user, String password) {
        ObjectPool pool = new GenericObjectPool(new SessionFactory(user, password),
                AbstractPoolingConnectionManager.this.maxActive,
                AbstractPoolingConnectionManager.this.whenExhaustedAction,
                AbstractPoolingConnectionManager.this.maxWait, AbstractPoolingConnectionManager.this.testOnBorrow,
                AbstractPoolingConnectionManager.this.testOnReturn);
        return pool;
    }

    /**
     * Creates a physical connection to your custom store. Needs to be
     * implemented by you as only you know how to do this.
     * 
     * @param user
     *            the name of the user who is requesting an authenticated
     *            session
     * @param password
     *            the password of the user who is requesting an authenticated
     *            session
     * @return the connection or whatever giving acccess to your custom store
     * @throws Exception
     *             in case authentication to your custom store failed or
     *             anything else went wrong
     */
    protected abstract Object createPhysicalConnection(String user, String password) throws Exception;

    /**
     * Closes a physical connection created by
     * {@link #createPhysicalConnection(String, String)}. This really closes
     * the connection physically and will only be called when the pool decides
     * it really does not need the connection any more. It is thus <em>not</em>
     * directly linked to {@link #closeAuthenticationSession(Object)}.
     * 
     * @param session
     *            the physical connection to be closed
     * @throws Exception
     *             in case anything went wrong
     */
    protected abstract void closePhysicalConnection(Object session) throws Exception;

    /**
     * Tries to wake up a physical connection that might have been in some sort
     * of sleeping mode before. This corresondents to activation of the pooled
     * object. Note that there is no need to update the user information on the
     * connection as pools are maintained per user / password combination. If
     * waking up was not successful, do not throw an exception, but rather let
     * {@link #isPhysicalConnectionValid(Object)}return <code>false</code>
     * which will eventualy be called as well.
     * 
     * @param session
     *            the physical connection to waken up
     * @throws Exception
     *             in case anything went wrong
     */
    protected abstract void wakeupPhysicalConnection(Object session) throws Exception;

    /**
     * Invalidates - passivates in the pool language - a physical connection
     * that has been in active before. of sleeping mode before. Note that there
     * is no need to reset the user information on the connection as pools are
     * maintained per user / password combination.
     * 
     * @param session
     *            the physical connection to be invalidated
     * @throws Exception
     *             in case anything went wrong
     */
    protected abstract void invalidatePhysicalConnection(Object session) throws Exception;

    /**
     * Checks if the physical connection can (still) be used to get access to
     * the custom store. Reasons for a connection be become invalid are for
     * example timeouts, server restarts, internal error, etc.
     * 
     * @param session
     *            the physical connection that might be invalid
     * @return <code>true</code> if the connection can (still) be used for
     *         accessing the custom store, <code>false</code> otherwise
     */
    protected abstract boolean isPhysicalConnectionValid(Object session);

    protected class SessionFactory implements PoolableObjectFactory {

        String user, password;

        public SessionFactory(String user, String password) {
            this.user = user;
            this.password = password;
        }

        public Object makeObject() throws Exception {
            Object session = AbstractPoolingConnectionManager.this.createPhysicalConnection(user, password);
            return session;
        }

        public void destroyObject(Object obj) throws Exception {
            AbstractPoolingConnectionManager.this.closePhysicalConnection(obj);
        }

        public boolean validateObject(Object obj) {
            boolean isValid = AbstractPoolingConnectionManager.this.isPhysicalConnectionValid(obj);
            return isValid;
        }

        public void activateObject(Object obj) throws Exception {
            AbstractPoolingConnectionManager.this.wakeupPhysicalConnection(obj);
        }

        public void passivateObject(Object obj) throws Exception {
            AbstractPoolingConnectionManager.this.invalidatePhysicalConnection(obj);
        }

    }

}