/*
 * Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License, Version
 * 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html). Initial Developer: H2 Group
 */
package org.h2.util;

import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.XADataSource;
import org.h2.engine.Constants;
import org.h2.jdbcx.JdbcDataSource;
import org.osgi.service.jdbc.DataSourceFactory;

/**
 * This class implements the OSGi DataSourceFactory interface for the H2 JDBC
 * driver. The following standard configuration properties are supported:
 * {@link #JDBC_USER}, {@link #JDBC_PASSWORD}, {@link #JDBC_DESCRIPTION},
 * {@link #JDBC_DATASOURCE_NAME}, {@link #JDBC_NETWORK_PROTOCOL},
 * {@link #JDBC_URL}, {@link #JDBC_SERVER_NAME}, {@link #JDBC_PORT_NUMBER}. The
 * following standard configuration properties are not supported:
 * {@link #JDBC_ROLE_NAME}, {@link #JDBC_DATABASE_NAME},
 * {@link #JDBC_INITIAL_POOL_SIZE}, {@link #JDBC_MAX_POOL_SIZE},
 * {@link #JDBC_MIN_POOL_SIZE}, {@link #JDBC_MAX_IDLE_TIME},
 * {@link #JDBC_MAX_STATEMENTS}, {@link #JDBC_PROPERTY_CYCLE}. Any other
 * property will be treated as a H2 specific option. If the {@link #JDBC_URL}
 * property is passed to any of the DataSource factories, the following
 * properties will be ignored: {@link #JDBC_DATASOURCE_NAME},
 * {@link #JDBC_NETWORK_PROTOCOL}, {@link #JDBC_SERVER_NAME},
 * {@link #JDBC_PORT_NUMBER}.
 *
 * @author Per Otterstrom
 */
public class OsgiDataSourceFactory implements DataSourceFactory {
    private org.h2.Driver driver;

    public OsgiDataSourceFactory(org.h2.Driver driver) {
        this.driver = driver;
    }

    /**
     * Creates a basic data source.
     *
     * @param properties the properties for the data source.
     * @throws SQLException if unsupported properties are supplied, or if data
     *             source can not be created.
     * @return a new data source.
     */
    public DataSource createDataSource(Properties properties) throws SQLException {
        // Make copy of properties
        Properties propertiesCopy = new Properties();
        if (properties != null) {
            propertiesCopy.putAll(properties);
        }

        // Verify that no unsupported standard options are used
        rejectUnsupportedOptions(propertiesCopy);

        // Standard pool properties in OSGi not applicable here
        rejectPoolingOptions(propertiesCopy);

        JdbcDataSource dataSource = new JdbcDataSource();

        setupH2DataSource(dataSource, propertiesCopy);

        return dataSource;
    }

    /**
     * Creates a pooled data source.
     *
     * @param properties the properties for the data source.
     * @throws SQLException if unsupported properties are supplied, or if data
     *             source can not be created.
     * @return a new data source.
     */
    public ConnectionPoolDataSource createConnectionPoolDataSource(Properties properties) throws SQLException {
        // Make copy of properties
        Properties propertiesCopy = new Properties();
        if (properties != null) {
            propertiesCopy.putAll(properties);
        }

        // Verify that no unsupported standard options are used
        rejectUnsupportedOptions(propertiesCopy);

        // The integrated connection pool is H2 is not configurable
        rejectPoolingOptions(propertiesCopy);

        JdbcDataSource dataSource = new JdbcDataSource();

        setupH2DataSource(dataSource, propertiesCopy);

        return dataSource;
    }

    /**
     * Creates a pooled XA data source.
     *
     * @param properties the properties for the data source.
     * @throws SQLException if unsupported properties are supplied, or if data
     *             source can not be created.
     * @return a new data source.
     */
    public XADataSource createXADataSource(Properties properties) throws SQLException {
        // Make copy of properties
        Properties propertiesCopy = new Properties();
        if (properties != null) {
            propertiesCopy.putAll(properties);
        }

        // Verify that no unsupported standard options are used
        rejectUnsupportedOptions(propertiesCopy);

        // The integrated connection pool is H2 is not configurable
        rejectPoolingOptions(propertiesCopy);

        JdbcDataSource dataSource = new JdbcDataSource();

        setupH2DataSource(dataSource, propertiesCopy);

        return dataSource;
    }

    /**
     * Returns a driver. The H2 driver does not support any properties.
     *
     * @param properties must be null or empty list.
     * @throws SQLException if any property is supplied.
     * @return a driver.
     */
    public java.sql.Driver createDriver(Properties properties) throws SQLException {
        if (properties != null && !properties.isEmpty()) {
            // No properties supported
            throw new SQLException();
        }
        return driver;
    }

    /**
     * Checker method that will throw if any unsupported standard OSGi options
     * is present.
     *
     * @param properties the properties to check
     * @throws SQLFeatureNotSupportedException if unsupported properties are
     *             present
     */
    private static void rejectUnsupportedOptions(Properties properties) throws SQLFeatureNotSupportedException {
        // Unsupported standard properties in OSGi
        if (properties.containsKey(DataSourceFactory.JDBC_ROLE_NAME)) {
            throw new SQLFeatureNotSupportedException("The " + DataSourceFactory.JDBC_ROLE_NAME
                    + " property is not supported by H2");
        }
        if (properties.containsKey(DataSourceFactory.JDBC_DATASOURCE_NAME)) {
            throw new SQLFeatureNotSupportedException("The " + DataSourceFactory.JDBC_DATASOURCE_NAME
                    + " property is not supported by H2");
        }
    }

    /**
     * Applies common OSGi properties to a H2 data source. Non standard
     * properties will be applied as H2 options.
     *
     * @param dataSource the data source to configure
     * @param properties the properties to apply to the data source
     */
    private static void setupH2DataSource(JdbcDataSource dataSource, Properties properties) {
        // Setting user and password
        if (properties.containsKey(DataSourceFactory.JDBC_USER)) {
            dataSource.setUser((String) properties.remove(DataSourceFactory.JDBC_USER));
        }
        if (properties.containsKey(DataSourceFactory.JDBC_PASSWORD)) {
            dataSource.setPassword((String) properties.remove(DataSourceFactory.JDBC_PASSWORD));
        }

        // Setting description
        if (properties.containsKey(DataSourceFactory.JDBC_DESCRIPTION)) {
            dataSource.setDescription((String) properties.remove(DataSourceFactory.JDBC_DESCRIPTION));
        }

        // Setting URL
        StringBuffer connectionUrl = new StringBuffer();
        if (properties.containsKey(DataSourceFactory.JDBC_URL)) {
            // Use URL if specified
            connectionUrl.append(properties.remove(DataSourceFactory.JDBC_URL));
            // Remove individual properties
            properties.remove(DataSourceFactory.JDBC_NETWORK_PROTOCOL);
            properties.remove(DataSourceFactory.JDBC_SERVER_NAME);
            properties.remove(DataSourceFactory.JDBC_PORT_NUMBER);
            properties.remove(DataSourceFactory.JDBC_DATABASE_NAME);
        } else {
            // Creating URL from individual properties
            connectionUrl.append(Constants.START_URL);

            // Set network protocol (tcp/ssl) or DB type (mem/file)
            String protocol = "";
            if (properties.containsKey(DataSourceFactory.JDBC_NETWORK_PROTOCOL)) {
                protocol = (String) properties.remove(DataSourceFactory.JDBC_NETWORK_PROTOCOL);
                connectionUrl.append(protocol).append(":");
            }

            // Host name and/or port
            if (properties.containsKey(DataSourceFactory.JDBC_SERVER_NAME)) {
                connectionUrl.append("//").append(properties.remove(DataSourceFactory.JDBC_SERVER_NAME));

                if (properties.containsKey(DataSourceFactory.JDBC_PORT_NUMBER)) {
                    connectionUrl.append(":").append(properties.remove(DataSourceFactory.JDBC_PORT_NUMBER));
                }

                connectionUrl.append("/");
            } else if (properties.containsKey(DataSourceFactory.JDBC_PORT_NUMBER)) {
                // Assume local host if only port was set
                connectionUrl.append("//localhost:").append(properties.remove(DataSourceFactory.JDBC_PORT_NUMBER))
                        .append("/");
            } else if (protocol.equals("tcp") || protocol.equals("ssl")) {
                // Assume local host if network protocol is set, but no host or
                // port is set
                connectionUrl.append("//localhost/");
            }

            // DB path and name
            if (properties.containsKey(DataSourceFactory.JDBC_DATABASE_NAME)) {
                connectionUrl.append(properties.remove(DataSourceFactory.JDBC_DATABASE_NAME));
            }
        }

        // Add remaining properties as options
        for (Object option : properties.keySet()) {
            connectionUrl.append(";").append(option).append("=").append(properties.get(option));
        }

        if (connectionUrl.length() > Constants.START_URL.length()) {
            dataSource.setURL(connectionUrl.toString());
        }
    }

    /**
     * Checker method that will throw if any pooling related standard OSGi
     * options are present.
     *
     * @param properties the properties to check
     * @throws SQLFeatureNotSupportedException if unsupported properties are
     *             present
     */
    private static void rejectPoolingOptions(Properties properties) throws SQLFeatureNotSupportedException {
        if (properties.containsKey(DataSourceFactory.JDBC_INITIAL_POOL_SIZE)
                || properties.containsKey(DataSourceFactory.JDBC_MAX_IDLE_TIME)
                || properties.containsKey(DataSourceFactory.JDBC_MAX_POOL_SIZE)
                || properties.containsKey(DataSourceFactory.JDBC_MAX_STATEMENTS)
                || properties.containsKey(DataSourceFactory.JDBC_MIN_POOL_SIZE)
                || properties.containsKey(DataSourceFactory.JDBC_PROPERTY_CYCLE)) {
            throw new SQLFeatureNotSupportedException("Pooling properties are not supported by H2");
        }
    }
}
