/*
 * Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.util;

import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Hashtable;
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.framework.BundleContext;
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 final 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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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.
     */
    @Override
    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 p the properties to check
     * @throws SQLFeatureNotSupportedException if unsupported properties are
     *             present
     */
    private static void rejectUnsupportedOptions(Properties p)
            throws SQLFeatureNotSupportedException {
        // Unsupported standard properties in OSGi
        if (p.containsKey(DataSourceFactory.JDBC_ROLE_NAME)) {
            throw new SQLFeatureNotSupportedException("The " +
                    DataSourceFactory.JDBC_ROLE_NAME +
                    " property is not supported by H2");
        }
        if (p.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 p the properties to apply to the data source
     */
    private static void setupH2DataSource(JdbcDataSource dataSource,
            Properties p) {
        // Setting user and password
        if (p.containsKey(DataSourceFactory.JDBC_USER)) {
            dataSource.setUser((String) p.remove(DataSourceFactory.JDBC_USER));
        }
        if (p.containsKey(DataSourceFactory.JDBC_PASSWORD)) {
            dataSource.setPassword((String) p
                    .remove(DataSourceFactory.JDBC_PASSWORD));
        }

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

        // Setting URL
        StringBuilder connectionUrl = new StringBuilder();
        if (p.containsKey(DataSourceFactory.JDBC_URL)) {
            // Use URL if specified
            connectionUrl.append(p.remove(DataSourceFactory.JDBC_URL));
            // Remove individual properties
            p.remove(DataSourceFactory.JDBC_NETWORK_PROTOCOL);
            p.remove(DataSourceFactory.JDBC_SERVER_NAME);
            p.remove(DataSourceFactory.JDBC_PORT_NUMBER);
            p.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 (p.containsKey(DataSourceFactory.JDBC_NETWORK_PROTOCOL)) {
                protocol = (String) p.remove(DataSourceFactory.JDBC_NETWORK_PROTOCOL);
                connectionUrl.append(protocol).append(":");
            }

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

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

                connectionUrl.append("/");
            } else if (p.containsKey(
                    DataSourceFactory.JDBC_PORT_NUMBER)) {
                // Assume local host if only port was set
                connectionUrl
                        .append("//localhost:")
                        .append(p.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 (p.containsKey(DataSourceFactory.JDBC_DATABASE_NAME)) {
                connectionUrl.append(
                        p.remove(DataSourceFactory.JDBC_DATABASE_NAME));
            }
        }

        // Add remaining properties as options
        for (Object option : p.keySet()) {
            connectionUrl.append(";").append(option).append("=")
                    .append(p.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 p the properties to check
     * @throws SQLFeatureNotSupportedException if unsupported properties are
     *             present
     */
    private static void rejectPoolingOptions(Properties p)
            throws SQLFeatureNotSupportedException {
        if (p.containsKey(DataSourceFactory.JDBC_INITIAL_POOL_SIZE) ||
                p.containsKey(DataSourceFactory.JDBC_MAX_IDLE_TIME) ||
                p.containsKey(DataSourceFactory.JDBC_MAX_POOL_SIZE) ||
                p.containsKey(DataSourceFactory.JDBC_MAX_STATEMENTS) ||
                p.containsKey(DataSourceFactory.JDBC_MIN_POOL_SIZE) ||
                p.containsKey(DataSourceFactory.JDBC_PROPERTY_CYCLE)) {
            throw new SQLFeatureNotSupportedException(
                    "Pooling properties are not supported by H2");
        }
    }

    /**
     * Register the H2 JDBC driver service.
     *
     * @param bundleContext the bundle context
     * @param driver the driver
     */
    static void registerService(BundleContext bundleContext,
            org.h2.Driver driver) {
        Hashtable<String, Object> properties = new Hashtable<>();
        properties.put(
                DataSourceFactory.OSGI_JDBC_DRIVER_CLASS,
                org.h2.Driver.class.getName());
        properties.put(
                DataSourceFactory.OSGI_JDBC_DRIVER_NAME,
                "H2 JDBC Driver");
        properties.put(
                DataSourceFactory.OSGI_JDBC_DRIVER_VERSION,
                Constants.FULL_VERSION);
        properties.put(DataSourceFactory.OSGI_JDBC_CAPABILITY, new String[] {
                DataSourceFactory.OSGI_JDBC_CAPABILITY_DRIVER,
                DataSourceFactory.OSGI_JDBC_CAPABILITY_DATASOURCE,
                DataSourceFactory.OSGI_JDBC_CAPABILITY_CONNECTIONPOOLDATASOURCE,
                DataSourceFactory.OSGI_JDBC_CAPABILITY_XADATASOURCE
        });
        bundleContext.registerService(
                DataSourceFactory.class.getName(),
                new OsgiDataSourceFactory(driver), properties);
    }
}
