/*
 * $Header: /esup-portail/injac/applications/SlideServer/Packages/esup-serveur-WebDav-3/update/slide/add-ons/stores/uPortalStore/src/stores/org/apache/slide/store/impl/rdbms/JDBCStore.java,v 1.1 2005/02/08 09:23:45 tbellemb Exp $
 * $Revision: 1.1 $
 * $Date: 2005/02/08 09:23:45 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 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.store.impl.rdbms;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Hashtable;

import org.apache.commons.dbcp.DriverManagerConnectionFactory;
import org.apache.commons.dbcp.PoolableConnectionFactory;
import org.apache.commons.dbcp.PoolingDriver;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.ServiceInitializationFailedException;
import org.apache.slide.common.ServiceParameterErrorException;
import org.apache.slide.common.ServiceParameterMissingException;
import org.apache.slide.store.ContentStore;
import org.apache.slide.store.LockStore;
import org.apache.slide.store.NodeStore;
import org.apache.slide.store.RevisionDescriptorStore;
import org.apache.slide.store.RevisionDescriptorsStore;
import org.apache.slide.store.SecurityStore;
import org.apache.slide.util.logger.Logger;

/**
 * Store implementation that is able to store all information (like structure,
 * locks and content) in a JDBC-aware relational database system. As this 
 * implementation only uses a single connection to the database, it does not 
 * work properly in production environments that require simultaneous access by
 * multiple clients.
 * 
 * @version $Revision: 1.1 $
 */
public class JDBCStore
    extends AbstractRDBMSStore
    implements LockStore, NodeStore, RevisionDescriptorsStore, RevisionDescriptorStore, SecurityStore, ContentStore {

    public static final String DBCP_URL = "jdbc:apache:commons:dbcp";

    public static final String TRANSACTION_NONE = "NONE";
    public static final String TRANSACTION_READ_UNCOMMITTED = "READ_UNCOMMITTED";
    public static final String TRANSACTION_READ_COMMITTED = "READ_COMMITTED";
    public static final String TRANSACTION_REPEATABLE_READ = "REPEATABLE_READ";
    public static final String TRANSACTION_SERIALIZABLE = "SERIALIZABLE";

    public static final int DEFAUT_ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED;

    protected static String isolationLevelToString(int isolationLevel) {
        String levelString;
        switch (isolationLevel) {
            case Connection.TRANSACTION_NONE :
                levelString = TRANSACTION_NONE;
                break;
            case Connection.TRANSACTION_READ_UNCOMMITTED :
                levelString = TRANSACTION_READ_UNCOMMITTED;
                break;
            case Connection.TRANSACTION_READ_COMMITTED :
                levelString = TRANSACTION_READ_COMMITTED;
                break;
            case Connection.TRANSACTION_REPEATABLE_READ :
                levelString = TRANSACTION_REPEATABLE_READ;
                break;
            case Connection.TRANSACTION_SERIALIZABLE :
                levelString = TRANSACTION_SERIALIZABLE;
                break;
            default :
                levelString = "UNKNOWN";
                break;
        }
        return levelString;

    }

    protected static int stringToIsolationLevelToString(String levelString) {
        if (TRANSACTION_NONE.equals(levelString)) {
            return Connection.TRANSACTION_NONE;
        } else if (TRANSACTION_READ_UNCOMMITTED.equals(levelString)) {
            return Connection.TRANSACTION_READ_UNCOMMITTED;
        } else if (TRANSACTION_READ_COMMITTED.equals(levelString)) {
            return Connection.TRANSACTION_READ_COMMITTED;
        } else if (TRANSACTION_REPEATABLE_READ.equals(levelString)) {
            return Connection.TRANSACTION_REPEATABLE_READ;
        } else if (TRANSACTION_SERIALIZABLE.equals(levelString)) {
            return Connection.TRANSACTION_SERIALIZABLE;
        } else {
            return -1;
        }
    }

    // ----------------------------------------------------- Instance Variables

    /**
     * Driver class name.
     */
    protected String driver;

    /**
     * Connection URL.
     */
    protected String url;

    /**
     * User name.
     */
    protected String user = "";

    /**
     * Password.
     */
    protected String password = "";

    protected boolean useDbcpPooling = false;

    protected String dbcpPoolName = "dbcpPool" + System.identityHashCode(this);

    protected int maxPooledConnections = -1;

    protected int isolationLevel = DEFAUT_ISOLATION_LEVEL;

    // -------------------------------------------------------- Service Methods

    /**
     * Initializes the data source with a set of parameters.
     *
     * @param parameters a Hashtable containing the parameters' name and 
     *                   associated value
     * @exception ServiceParameterErrorException a service parameter holds an 
     *            invalid value
     * @exception ServiceParameterMissingException a required parameter is 
     *            missing
     */
    public void setParameters(Hashtable parameters)
        throws ServiceParameterErrorException, ServiceParameterMissingException {

        String value;

        // Driver classname
        value = (String) parameters.get("driver");
        if (value == null) {
            throw new ServiceParameterMissingException(this, "driver");
        } else {
            driver = value;
        }

        // Connection url
        value = (String) parameters.get("url");
        if (value == null) {
            throw new ServiceParameterMissingException(this, "url");
        } else {
            url = value;
        }

        // User name
        value = (String) parameters.get("user");
        if (value != null) {
            user = value;
        }

        // Password
        value = (String) parameters.get("password");
        if (value != null) {
            password = value;
        }

        value = (String) parameters.get("isolation");
        if (value != null) {
            isolationLevel = stringToIsolationLevelToString(value);
            if (isolationLevel == -1) {
                getLogger().log(
                    "Could not set isolation level '"
                        + value
                        + "', allowed levels are "
                        + TRANSACTION_NONE
                        + ", "
                        + TRANSACTION_READ_UNCOMMITTED
                        + ", "
                        + TRANSACTION_READ_COMMITTED
                        + ", "
                        + TRANSACTION_REPEATABLE_READ
                        + ", "
                        + TRANSACTION_SERIALIZABLE,
                    LOG_CHANNEL,
                    Logger.WARNING);
                isolationLevel = DEFAUT_ISOLATION_LEVEL;
            }
        }

        value = (String) parameters.get("dbcpPooling");
        if (value != null) {
            useDbcpPooling = "true".equals(value);
        }

        if (useDbcpPooling) {
            value = (String) parameters.get("maxPooledConnections");
            if (value != null) {
                try {
                    maxPooledConnections = Integer.parseInt(value);
                } catch (NumberFormatException nfe) {
                    getLogger().log(
                        "Could not set maximum pooled connections, parameter must be integer",
                        LOG_CHANNEL,
                        Logger.WARNING);

                }
            }
        }

        super.setParameters(parameters);
    }

    /**
     * Initializes driver.
     * <p/>
     * Occurs in four steps :
     * <li>Driver class is loaded</li>
     * <li>Driver is instantiated</li>
     * <li>Driver registration in the driver manager</li>
     * <li>Creation of the basic tables, if they didn't exist before</li>
     * 
     * @exception ServiceInitializationFailedException Throws an exception 
     * if the data source has already been initialized before
     */
    public synchronized void initialize(NamespaceAccessToken token) throws ServiceInitializationFailedException {

        // XXX might be done already in setParameter
        if (!alreadyInitialized) {
            try {
                // Loading and registering driver
                getLogger().log("Loading and registering driver '" + driver + "'", LOG_CHANNEL, Logger.INFO);
                Class driverClass = Class.forName(driver);
                Driver driverInstance = (Driver) driverClass.newInstance();

                String levelString = isolationLevelToString(isolationLevel);

                getLogger().log("Setting isolation level '" + levelString + "'", LOG_CHANNEL, Logger.INFO);

                // use DBCP pooling if enabled
                if (useDbcpPooling) {
                    getLogger().log("Using DBCP pooling", LOG_CHANNEL, Logger.INFO);
                    GenericObjectPool connectionPool = new GenericObjectPool(null);
                    if (maxPooledConnections != -1) {
                        connectionPool.setMaxActive(maxPooledConnections);
                    }
                    getLogger().log(
                        "Number of connections set to " + connectionPool.getMaxActive(),
                        LOG_CHANNEL,
                        Logger.INFO);

                    DriverManagerConnectionFactory connectionFactory =
                        new DriverManagerConnectionFactory(url, user, password);
                    new PoolableConnectionFactory(
                        connectionFactory,
                        connectionPool,
                        // TODO switching on pooling of prepared statements causes problems with closing of connections
                        // switched off for now
//                  new StackKeyedObjectPoolFactory(),
                        null, 
                        null,
                        false,
                        false,
                        isolationLevel);
                    PoolingDriver driver = new PoolingDriver();
                    driver.registerPool(dbcpPoolName, connectionPool);
                    // already done when loding PoolingDriver class 
                    //                DriverManager.registerDriver(driver);
                } else {
                    DriverManager.registerDriver(driverInstance);
                    getLogger().log("Not using DBCP pooling", LOG_CHANNEL, Logger.WARNING);
                }
            } catch (Exception e) {
                getLogger().log(
                    "Loading and registering driver '" + driver + "' failed (" + e.getMessage() + ")",
                    LOG_CHANNEL,
                    Logger.ERROR);
                throw new ServiceInitializationFailedException(this, e);
            } finally {
                alreadyInitialized = true;
            }
        }

    }

    protected Connection getNewConnection() throws SQLException {

        Connection connection;
        if (useDbcpPooling) {
            try {
                connection = DriverManager.getConnection(DBCP_URL + ":" + dbcpPoolName);
            } catch (SQLException e) {
                getLogger().log("Could not create connection. Reason: " + e, LOG_CHANNEL, Logger.EMERGENCY);
                throw e;
            }
        } else {
            try {
                connection = DriverManager.getConnection(url, user, password);
            } catch (SQLException e) {
                getLogger().log("Could not create connection. Reason: " + e, LOG_CHANNEL, Logger.EMERGENCY);
                throw e;
            }

            try {
                if (connection.getTransactionIsolation() != isolationLevel) {
                    connection.setTransactionIsolation(isolationLevel);
                }
            } catch (SQLException e) {
                getLogger().log(
                    "Could not set isolation level '" + isolationLevelToString(isolationLevel) + "'. Reason: " + e,
                    LOG_CHANNEL,
                    Logger.WARNING);
            }

            if (connection.getAutoCommit()) {
                connection.setAutoCommit(false);
            }

        }

        return connection;
    }

    protected boolean includeBranchInXid() {
        return false;
    }
}
