/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/impl/rdbms/JDBCStore.java,v 1.24 2005/06/13 12:55:20 unico Exp $
 * $Revision: 1.24 $
 * $Date: 2005/06/13 12:55:20 $
 *
 * ====================================================================
 *
 * Copyright 1999-2005 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 java.util.Iterator;
import java.util.Properties;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
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.common.ServiceDisconnectionFailedException;
import org.apache.slide.common.ServiceConnectionFailedException;
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.
 * <p>
 * Set the {@link #CONF_KEY_DBCP_ENABLED} configuration attribute to true
 * to enable Commons DBCP connection pooling.
 *
 * @version $Revision: 1.24 $
 * @see <a href="http://jakarta.apache.org/slide/howto-j2eestore.html">Slide Configuration HOWTO</a>
 * for complete configuration information
 */
public class JDBCStore
    extends AbstractRDBMSStore
    implements LockStore, NodeStore, RevisionDescriptorsStore, RevisionDescriptorStore, SecurityStore, ContentStore {

    /** Configuration key: "true"=use Commons DBCP connection pooling. */
    protected static final String CONF_KEY_DBCP_ENABLED = "dbcpPooling";

    /**
     * Configuration key prefix for all Commons DBCP settings.
     * To pass properties directly to the underlying JDBC-driver,
     * use the <b>{@link #CONF_KEY_DBCP_PREFIX}.connectionProperties</b>
     * configuration property in your Slide domain configuration,
     * set the value to semicolon-separated name=value pairs
     * (eg "cachePreparedStatements=true;rowPrefetchSize=50;foo=bar").
     * @see <a href="http://jakarta.apache.org/commons/dbcp/configuration.html">
     * Commons DBCP website</a> for info about possible settings
     */
    protected static final String CONF_KEY_DBCP_PREFIX = "dbcp.";

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

    public static final int DEFAUT_ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED;

    protected static String isolationLevelToString(int isolationLevel) {
        final 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) {
        final int transactionIsolationLevel;
        if (TRANSACTION_NONE.equals(levelString)) {
            transactionIsolationLevel = Connection.TRANSACTION_NONE;
        } else if (TRANSACTION_READ_UNCOMMITTED.equals(levelString)) {
            transactionIsolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED;
        } else if (TRANSACTION_READ_COMMITTED.equals(levelString)) {
            transactionIsolationLevel = Connection.TRANSACTION_READ_COMMITTED;
        } else if (TRANSACTION_REPEATABLE_READ.equals(levelString)) {
            transactionIsolationLevel = Connection.TRANSACTION_REPEATABLE_READ;
        } else if (TRANSACTION_SERIALIZABLE.equals(levelString)) {
            transactionIsolationLevel = Connection.TRANSACTION_SERIALIZABLE;
        } else {
            transactionIsolationLevel = -1;
        }
        return transactionIsolationLevel;
    }

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

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

    /** JDBC connection URL. */
    protected String url;

    /** Database user-name. */
    protected String user;

    /** Database password. */
    protected String password;

    /** Transaction isolation level. */
    protected int isolationLevel;

    /** Switch for enabling Commons DBCP connection pooling. */
    protected boolean useDbcpPooling;

    /** Properties set from Store configuration, to be used with DBCP. */
    protected Properties dbcpProperties;

    /** Connection pool DataSource (set only if using Commons DBCP). */
    protected BasicDataSource dbcpDataSource;

    // -------------------------------------------------------- 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
        user = "";
        value = (String) parameters.get("user");
        if (value != null) {
            user = value;
        }

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

        // Transaction isolation level
        isolationLevel = DEFAUT_ISOLATION_LEVEL;
        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;
            }
        }

        // Connection pooling
        useDbcpPooling = false;
        value = (String) parameters.get(CONF_KEY_DBCP_ENABLED);
        if (value != null) {
            useDbcpPooling = "true".equals(value);
        }

        if (useDbcpPooling) {
            // Set up configuration properties to be used with Commons DBCP
            dbcpProperties = getDbcpPoolProperties(parameters);

            // Add driver and logon information
            dbcpProperties.put("driverClassName", driver);
            dbcpProperties.put("url", url);
            dbcpProperties.put("username", user);
            dbcpProperties.put("password", password);

            // Set TX isolation level
            final String isolationLevelString;
            isolationLevelString = isolationLevelToString(isolationLevel);
            dbcpProperties.put("defaultTransactionIsolation",
                isolationLevelString);

            // Turn off autocommit
            dbcpProperties.put("defaultAutoCommit",
                String.valueOf(Boolean.FALSE));
        }

        super.setParameters(parameters);
    }

    /**
     * Initializes this store.
     * <p/>
     * If Commons DBCP is used, initialization is done by:
     *  <ol>
     *   <li>Loading JDBC-driver class (and running static initializers)</li>
     * </ol>
     * DBCP will handled DriverManager registration internally, which will
     * (together with further initialization of Connection pool) occur in
     * the {@link #connect} method.
     * <p/>
     * Otherwise (ie not using DBCP), initialization is done by:
     *  <ol>
     *   <li>Loading JDBC-driver class (and running static initializers)</li>
     *   <li>Instantiating Driver instance</li>
     *   <li>Driver registration in the JDBC DriverManager</li>
     *  </ol>
     *
     * @exception ServiceInitializationFailedException if JDBC driver
     * classloading or registration fails
     */
    public synchronized void initialize(NamespaceAccessToken token) throws ServiceInitializationFailedException {

        // XXX might be done already in setParameter
        if (!alreadyInitialized) {
            try {
                // Load driver class and force static init
                getLogger().log("Loading and registering driver '" + driver + "'", LOG_CHANNEL, Logger.INFO);

                final ClassLoader cl = Thread.currentThread().getContextClassLoader();
                final Class driverClass = Class.forName(driver, true, cl);

                // Use Commons DBCP pooling if enabled
                if (useDbcpPooling) {
                    getLogger().log("Using DBCP pooling", LOG_CHANNEL, Logger.INFO);
                } else {
                    // Register Driver instance with JDBC DriverManager
                    final Driver driverInstance = (Driver) driverClass.newInstance();
                    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;
            }
        }
    }

    public void connect() throws ServiceConnectionFailedException {
        super.connect();
        if (useDbcpPooling) {
            try {
                dbcpDataSource = (BasicDataSource)
                    BasicDataSourceFactory.createDataSource(dbcpProperties);

                // The BasicDataSource has lazy initialization
                // borrowing a connection will start the DataSource
                // and make sure it is configured correctly.
                final Connection conn = dbcpDataSource.getConnection();
                conn.close();
            } catch (Exception e) {
                getLogger().log(
                    "Initialization of connection pool failed ("
                    + e.getMessage() + ")",
                    LOG_CHANNEL, Logger.ERROR);
                throw new ServiceConnectionFailedException(this, e);
            } finally {
                logConnectionPoolStatistics();
            }
        }
    }

    public boolean isConnected() {
        if (useDbcpPooling) {
            return dbcpDataSource != null;
        }
        return super.isConnected();
    }

    public void disconnect() throws ServiceDisconnectionFailedException {
        if (useDbcpPooling) {
            if (dbcpDataSource != null) {
                try {
                    dbcpDataSource.close();
                } catch (SQLException e) {
                    getLogger().log(
                        "Decomissioning of connection pool failed ("
                        + e.getMessage() + ")",
                        LOG_CHANNEL, Logger.ERROR);
                    throw new ServiceDisconnectionFailedException(this, e);
                }
                dbcpDataSource = null;
            }
        }
        super.disconnect();
    }

    protected Connection getNewConnection() throws SQLException {
        final Connection connection;

        if (useDbcpPooling) {
            try {
                connection = dbcpDataSource.getConnection();
            } catch (SQLException e) {
                getLogger().log("Could not create connection. Reason: " + e, LOG_CHANNEL, Logger.EMERGENCY);
                throw e;
            } finally {
                logConnectionPoolStatistics();
            }
        } 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;
    }

    /**
     * Returns properties to be used with Commons DBCP BasicDataSource.
     *
     * @param parameters the configuration parameters for this store
     * @return properties to pass to the DBCP datasource, set up
     * according to store configuration parameters (never null)
     */
    protected Properties getDbcpPoolProperties(Hashtable parameters) {
        final Properties props = new Properties();

        if (parameters == null) {
            return props;
        }

        // Set all DBCP properties
        final int prefixSkipchars = CONF_KEY_DBCP_PREFIX.length();
        final Iterator keyIter = parameters.keySet().iterator();
        while (keyIter.hasNext()) {
            final String key = String.valueOf(keyIter.next());
            if (key.startsWith(CONF_KEY_DBCP_PREFIX)) {
                final String property = key.substring(prefixSkipchars);
                final Object value = parameters.get(key);
                if (value != null) {
                    props.put(property, value.toString());
                }
            }
        }

        int intValue = -2; // use -2, since -1 and 0 are magic numbers already
        // Old maxActive setting ("maxPooledConnections") have priority
        final Object value = parameters.get("maxPooledConnections");
        if (value != null) {
            try {
                intValue = Integer.parseInt(value.toString());
            } catch (NumberFormatException e) {
                getLogger().log("Could not parse configuration setting " +
                    "maxPooledConnections, parameter must be integer",
                    LOG_CHANNEL,
                    Logger.WARNING);
            }
        }
        if (intValue > -2) {
            getLogger().log("The maxPooledConnections configuration " +
                "attribute is deprecated, please use " +
                CONF_KEY_DBCP_PREFIX + "maxActive instead.",
                LOG_CHANNEL,
                Logger.WARNING);
            props.put("maxActive", value.toString());
        }
        return props;
    }

    /**
     * Log statistics for Connection pool
     * (when using Commons DBCP and log level DEBUG).
     */
    protected void logConnectionPoolStatistics() {
        // Avoid String concatenation if message is not going to be logged
        if (useDbcpPooling && getLogger().getLoggerLevel(LOG_CHANNEL) >= Logger.DEBUG) {
            getLogger().log("Connection pool stats: active: " +
                dbcpDataSource.getNumActive() + " (max: " +
                dbcpDataSource.getMaxActive() + "), idle: " +
                dbcpDataSource.getNumIdle() + "(max: " +
                dbcpDataSource.getMaxIdle() + ")",
                LOG_CHANNEL, Logger.DEBUG);
        }
    }

}
