# JNDIBasedDataSourceRepository.java

    /*
     *  Licensed to the Apache Software Foundation (ASF) under one
     *  or more contributor license agreements.  See the NOTICE file
     *  distributed with this work for additional information
     *  regarding copyright ownership.  The ASF licenses this file
     *  to you 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.
     */
    /**
     * To change this template use File | Settings | File Templates.
     */
    package org.apache.synapse.commons.datasource;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.synapse.commons.SynapseCommonsException;
    import org.apache.synapse.commons.util.MiscellaneousUtil;
    import org.apache.synapse.commons.util.RMIRegistryController;
    import org.wso2.securevault.SecurityConstants;

    import javax.naming.*;
    import javax.sql.DataSource;
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    import java.util.*;

    /**
     * Keep all DataSources in the JNDI Tree
     */
    public class JNDIBasedDataSourceRepository implements DataSourceRepository {

        private static Log log = LogFactory.getLog(JNDIBasedDataSourceRepository.class);

        private InitialContext initialContext;
        private Properties jndiProperties;
        private final Map<String, InitialContext> perDataSourceICMap
                = new HashMap<String, InitialContext>();
        private final List<String> cachedNameList = new ArrayList<String>();
        private final List<Integer> cachedPorts = new ArrayList<Integer>();

        private boolean initialized = false;

        public void init(Properties jndiEnv) {

            initialized = true;
            if (jndiEnv == null || jndiEnv.isEmpty()) {
                if (log.isDebugEnabled()) {
                    log.debug("Provided global JNDI environment properties is empty or null.");
                }
                return;
            }

            if (isValid(jndiEnv)) {
                jndiProperties = createJNDIEnvironment(jndiEnv, null);
                initialContext = createInitialContext(jndiProperties);
            }
        }

        /**
         * Register a DataSource in the JNDI tree
         *
         * @see DataSourceRepository#register(DataSourceInformation)
         */
        public void register(DataSourceInformation information) {

            validateInitialized();
            String dataSourceName = information.getDatasourceName();
            validateDSName(dataSourceName);
            Properties properties = information.getProperties();

            InitialContext context = null;
            Properties jndiEvn = null;

            if (properties == null || properties.isEmpty()) {
                if (initialContext != null) {
                    context = initialContext;
                    if (log.isDebugEnabled()) {
                        log.debug("Empty JNDI properties for datasource " + dataSourceName);
                        log.debug("Using system-wide jndi properties : " + jndiProperties);
                    }

                    jndiEvn = jndiProperties;
                }
            }

            if (context == null) {

                jndiEvn = createJNDIEnvironment(properties, information.getAlias());
                context = createInitialContext(jndiEvn);

                if (context == null) {

                    validateInitialContext(initialContext);
                    context = initialContext;

                    if (log.isDebugEnabled()) {
                        log.debug("Cannot create a name context with provided jndi properties : " +
                                jndiEvn);
                        log.debug("Using system-wide JNDI properties : " + jndiProperties);
                    }

                    jndiEvn = jndiProperties;
                } else {
                    perDataSourceICMap.put(dataSourceName, context);
                }
            }

            String dsType = information.getType();
            String driver = information.getDriver();
            String url = information.getUrl();

            String user = information.getSecretInformation().getUser();
            String password = information.getSecretInformation().getResolvedSecret();

            String maxActive = String.valueOf(information.getMaxActive());
            String maxIdle = String.valueOf(information.getMaxIdle());
            String maxWait = String.valueOf(information.getMaxWait());

            //populates context tree
            populateContextTree(context, dataSourceName);

            if (DataSourceInformation.BASIC_DATA_SOURCE.equals(dsType)) {

                Reference ref = new Reference("javax.sql.DataSource",
                        "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory", null);

                ref.add(new StringRefAddr(DataSourceConstants.PROP_DRIVER_CLS_NAME,
                        driver));
                ref.add(new StringRefAddr(DataSourceConstants.PROP_URL, url));
                ref.add(new StringRefAddr(SecurityConstants.PROP_USER_NAME, user));
                ref.add(new StringRefAddr(SecurityConstants.PROP_PASSWORD, password));
                ref.add(new StringRefAddr(DataSourceConstants.PROP_MAX_ACTIVE, maxActive));
                ref.add(new StringRefAddr(DataSourceConstants.PROP_MAX_IDLE, maxIdle));
                ref.add(new StringRefAddr(DataSourceConstants.PROP_MAX_WAIT, maxWait));

                // set BasicDataSource specific parameters
                setBasicDataSourceParameters(ref, information);
                //set default jndiProperties for reference
                setCommonParameters(ref, information);

                try {

                    if (log.isDebugEnabled()) {
                        log.debug("Registering a DataSource with name : " +
                                dataSourceName + " in the JNDI tree with jndiProperties : " + jndiEvn);
                    }

                    context.rebind(dataSourceName, ref);
                } catch (NamingException e) {
                    String msg = " Error binding name ' " + dataSourceName + " ' to " +
                            "the DataSource(BasicDataSource) reference";
                    throw new SynapseCommonsException(msg, e, log);
                }

            } else if (DataSourceInformation.PER_USER_POOL_DATA_SOURCE.equals(dsType)) {

                // Construct DriverAdapterCPDS reference
                String className = (String) information.getParameter(
                        DataSourceConstants.PROP_CPDS_ADAPTER +
                                DataSourceConstants.DOT_STRING +
                                DataSourceConstants.PROP_CPDS_CLASS_NAME);
                String factory = (String) information.getParameter(
                        DataSourceConstants.PROP_CPDS_ADAPTER +
                                DataSourceConstants.DOT_STRING +
                                DataSourceConstants.PROP_CPDS_FACTORY);
                String name = (String) information.getParameter(
                        DataSourceConstants.PROP_CPDS_ADAPTER +
                                DataSourceConstants.DOT_STRING +
                                DataSourceConstants.PROP_CPDS_NAME);

                Reference cpdsRef =
                        new Reference(className, factory, null);

                cpdsRef.add(new StringRefAddr(DataSourceConstants.PROP_DRIVER, driver));
                cpdsRef.add(new StringRefAddr(DataSourceConstants.PROP_URL, url));
                cpdsRef.add(new StringRefAddr(DataSourceConstants.PROP_USER, user));
                cpdsRef.add(new StringRefAddr(SecurityConstants.PROP_PASSWORD,
                        password));

                try {
                    context.rebind(name, cpdsRef);
                } catch (NamingException e) {
                    String msg = "Error binding name '" + name + "' to " +
                            "the DriverAdapterCPDS reference";
                    throw new SynapseCommonsException(msg, e, log);
                }

                // Construct PerUserPoolDataSource reference
                Reference ref = new Reference("org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSource",
                        "org.apache.tomcat.dbcp.dbcp2.datasources.PerUserPoolDataSourceFactory", null);

                ref.add(new BinaryRefAddr(
                        DataSourceConstants.PROP_JNDI_ENV,
                        MiscellaneousUtil.serialize(jndiEvn)));
                ref.add(new StringRefAddr(
                        DataSourceConstants.PROP_DATA_SOURCE_NAME, name));
                ref.add(new StringRefAddr(
                        DataSourceConstants.PROP_DEFAULT_MAX_ACTIVE, maxActive));
                ref.add(new StringRefAddr(
                        DataSourceConstants.PROP_DEFAULT_MAX_IDLE, maxIdle));
                ref.add(new StringRefAddr(
                        DataSourceConstants.PROP_DEFAULT_MAX_WAIT, maxWait));

                //set default jndiProperties for reference
                setCommonParameters(ref, information);

                try {

                    if (log.isDebugEnabled()) {
                        log.debug("Registering a DataSource with name : " +
                                dataSourceName + " in the JNDI tree with jndiProperties : " + jndiEvn);
                    }

                    context.rebind(dataSourceName, ref);
                } catch (NamingException e) {
                    String msg = "Error binding name ' " + dataSourceName + " ' to " +
                            "the PerUserPoolDataSource reference";
                    throw new SynapseCommonsException(msg, e, log);
                }

            } else {
                throw new SynapseCommonsException("Unsupported data source type : " + dsType, log);
            }
            cachedNameList.add(dataSourceName);
        }

        public void unRegister(String name) {

            InitialContext context = getCachedInitialContext(name);
            try {
                context.unbind(name);
            } catch (NamingException e) {
                throw new SynapseCommonsException("Error removing a Datasource with name : " +
                        name + " from the JNDI context : " + initialContext, e);
            }
            cachedNameList.remove(name);
        }

        /**
         * Get a DatSource which has been registered in the JNDI tree
         *
         * @see DataSourceRepository#lookUp(String)
         */
        public DataSource lookUp(String dsName) {

            validateInitialized();
            validateDSName(dsName);
            if (!cachedNameList.contains(dsName)) {
                if (log.isDebugEnabled()) {
                    log.debug("There is no Datasource with name " + dsName +
                            " in JNDI Datasource Repository");
                }
                return null;
            }
            if (log.isDebugEnabled()) {
                log.debug("Getting a DataSource with name : " + dsName + " from the JNDI tree.");
            }

            InitialContext context = getCachedInitialContext(dsName);
            return DataSourceFinder.find(dsName, context);
        }

        public void clear() {
            initialized = false;
            initialContext = null;
            jndiProperties.clear();
            perDataSourceICMap.clear();
            cachedNameList.clear();
            for (int port : cachedPorts) {
                RMIRegistryController.getInstance().removeLocalRegistry(port);
            }
            cachedPorts.clear();
        }

        private InitialContext getCachedInitialContext(String name) {
            InitialContext context = perDataSourceICMap.get(name);
            if (context == null) {
                validateInitialContext(initialContext);
                context = initialContext;
            }
            return context;
        }

        /**
         * Helper method to set all default parameter for naming reference of data source
         *
         * @param reference   The naming reference instance
         * @param information DataSourceInformation instance
         */
        private static void setCommonParameters(Reference reference, DataSourceInformation information) {

            reference.add(new StringRefAddr(DataSourceConstants.PROP_DEFAULT_AUTO_COMMIT,
                    String.valueOf(information.isDefaultAutoCommit())));
            reference.add(new StringRefAddr(DataSourceConstants.PROP_DEFAULT_READ_ONLY,
                    String.valueOf(information.isDefaultReadOnly())));
            reference.add(new StringRefAddr(DataSourceConstants.PROP_TEST_ON_BORROW,
                    String.valueOf(information.isTestOnBorrow())));
            reference.add(new StringRefAddr(DataSourceConstants.PROP_TEST_ON_RETURN,
                    String.valueOf(information.isTestOnReturn())));
            reference.add(new StringRefAddr(
                    DataSourceConstants.PROP_TIME_BETWEEN_EVICTION_RUNS_MILLIS,
                    String.valueOf(information.getTimeBetweenEvictionRunsMillis())));
            reference.add(new StringRefAddr(
                    DataSourceConstants.PROP_NUM_TESTS_PER_EVICTION_RUN,
                    String.valueOf(information.getNumTestsPerEvictionRun())));
            reference.add(new StringRefAddr(
                    DataSourceConstants.PROP_MIN_EVICTABLE_IDLE_TIME_MILLIS,
                    String.valueOf(information.getMinEvictableIdleTimeMillis())));
            reference.add(new StringRefAddr(
                    DataSourceConstants.PROP_TEST_WHILE_IDLE,
                    String.valueOf(information.isTestWhileIdle())));

            String validationQuery = information.getValidationQuery();

            if (validationQuery != null && !"".equals(validationQuery)) {
                reference.add(new StringRefAddr(
                        DataSourceConstants.PROP_VALIDATION_QUERY, validationQuery));
            }
        }

        /**
         * Helper method to set all BasicDataSource specific parameter
         *
         * @param ref         The naming reference instance
         * @param information DataSourceInformation instance
         */
        private static void setBasicDataSourceParameters(Reference ref,
                                                         DataSourceInformation information) {

            int defaultTransactionIsolation = information.getDefaultTransactionIsolation();
            String defaultCatalog = information.getDefaultCatalog();

            if (defaultTransactionIsolation != -1) {
                ref.add(new StringRefAddr(
                        DataSourceConstants.PROP_DEFAULT_TRANSACTION_ISOLATION,
                        String.valueOf(defaultTransactionIsolation)));
            }

            ref.add(new StringRefAddr(DataSourceConstants.PROP_MIN_IDLE,
                    String.valueOf(information.getMaxIdle())));
            ref.add(new StringRefAddr(
                    DataSourceConstants.PROP_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED,
                    String.valueOf(information.isAccessToUnderlyingConnectionAllowed())));
            ref.add(new StringRefAddr(
                    DataSourceConstants.PROP_REMOVE_ABANDONED,
                    String.valueOf(information.isRemoveAbandoned())));
            ref.add(new StringRefAddr(DataSourceConstants.PROP_REMOVE_ABANDONED_TIMEOUT,
                    String.valueOf(information.getRemoveAbandonedTimeout())));
            ref.add(new StringRefAddr(
                    DataSourceConstants.PROP_LOG_ABANDONED,
                    String.valueOf(information.isLogAbandoned())));
            ref.add(new StringRefAddr(
                    DataSourceConstants.PROP_POOL_PREPARED_STATEMENTS,
                    String.valueOf(information.isPoolPreparedStatements())));
            ref.add(new StringRefAddr(DataSourceConstants.PROP_MAX_OPEN_PREPARED_STATEMENTS,
                    String.valueOf(information.getMaxOpenPreparedStatements())));
            ref.add(new StringRefAddr(
                    DataSourceConstants.PROP_INITIAL_SIZE, String.valueOf(
                            information.getInitialSize())));

            if (defaultCatalog != null && !"".equals(defaultCatalog)) {
                ref.add(new StringRefAddr
                        (DataSourceConstants.PROP_DEFAULT_CATALOG, defaultCatalog));
            }
        }

        /**
         * Helper method to create context tree for a given path
         *
         * @param initialContext The root context
         * @param path           The path of the resource
         */
        private static void populateContextTree(InitialContext initialContext, String path) {

            String[] paths = path.split("/");
            if (paths != null && paths.length > 1) {

                Context context = initialContext;
                for (String path1 : paths) {

                    try {
                        assert context != null;
                        context = context.createSubcontext(path1);
                        if (context == null) {
                            throw new SynapseCommonsException("sub context " + path1 + " could not" +
                                    " be created", log);
                        }

                    } catch (NamingException e) {
                        throw new SynapseCommonsException("Unable to create sub context : " + path1,
                                e, log);
                    }
                }
            }
        }

        private Properties createJNDIEnvironment(Properties dsProperties, String name) {

            String namingFactory = DataSourceConstants.DEFAULT_IC_FACTORY;
            String providerUrl = null;
            int port = DataSourceConstants.DEFAULT_PROVIDER_PORT;
            String providerPort = null;
            // setting naming provider
            Properties jndiEvn = new Properties();  //This is needed for PerUserPoolDatasource

            if (dsProperties != null && !dsProperties.isEmpty()) {

                if (log.isDebugEnabled()) {
                    log.debug("Using properties " + dsProperties + " to create JNDI Environment");
                }

                StringBuffer buffer = new StringBuffer();
                buffer.append(DataSourceConstants.PROP_SYNAPSE_PREFIX_DS);
                buffer.append(DataSourceConstants.DOT_STRING);
                if (name != null && !"".equals(name)) {
                    buffer.append(name);
                    buffer.append(DataSourceConstants.DOT_STRING);
                }
                // The prefix for root level jndiProperties
                String rootPrefix = buffer.toString();

                namingFactory = MiscellaneousUtil.getProperty(
                        dsProperties, rootPrefix + DataSourceConstants.PROP_IC_FACTORY,
                        DataSourceConstants.DEFAULT_IC_FACTORY);

                //Provider URL
                providerUrl = MiscellaneousUtil.getProperty(
                        dsProperties, rootPrefix + DataSourceConstants.PROP_PROVIDER_URL,
                        null);
                providerPort =
                        MiscellaneousUtil.getProperty(dsProperties, rootPrefix +
                                DataSourceConstants.PROP_PROVIDER_PORT,
                                String.valueOf(DataSourceConstants.DEFAULT_PROVIDER_PORT));

            }

            jndiEvn.put(Context.INITIAL_CONTEXT_FACTORY, namingFactory);

            if (providerUrl != null && !"".equals(providerUrl)) {
                if (log.isDebugEnabled()) {
                    log.debug("Using provided initial context provider url :" + providerUrl);
                }

            } else {
                if (log.isDebugEnabled()) {
                    log.debug("No initial context provider url...creaeting a new one");
                }
                String providerHost = "localhost";
                try {
                    InetAddress addr = InetAddress.getLocalHost();
                    if (addr != null) {
                        String hostname = addr.getHostName();
                        if (hostname == null) {
                            String ipAddr = addr.getHostAddress();
                            if (ipAddr != null) {
                                providerHost = ipAddr;
                            }
                        } else {
                            providerHost = hostname;
                        }
                    }
                } catch (UnknownHostException e) {
                    log.warn("Unable to determine hostname or IP address.. Using localhost", e);
                }

                // default port for RMI registry

                if (providerPort != null) {
                    try {
                        port = Integer.parseInt(providerPort);
                    } catch (NumberFormatException ignored) {
                    }
                }

                // Create a RMI local registry
                RMIRegistryController.getInstance().createLocalRegistry(port);
                cachedPorts.add(port);
                providerUrl = "rmi://" + providerHost + ":" + port;

            }

            jndiEvn.put(Context.PROVIDER_URL, providerUrl);

            log.info("DataSources will be registered in the JNDI context with provider PROP_URL : " +
                    providerUrl);
            return jndiEvn;
        }

        public boolean isInitialized() {
            return initialized;
        }

        private void validateInitialized() {
            if (!isInitialized()) {
                throw new SynapseCommonsException("Datasource repository has not been initialized " +
                        "yet", log);
            }
        }

        private void validateDSName(String dataSourceName) {
            if (dataSourceName == null || "".equals(dataSourceName)) {
                throw new SynapseCommonsException("Invalid DataSource configuration !! -" +
                        "DataSource Name cannot be found ", log);
            }
        }

        private void validateInitialContext(InitialContext initialContext) {
            if (initialContext == null) {
                throw new SynapseCommonsException("InitialContext cannot be found.", log);
            }
        }

        private InitialContext createInitialContext(Properties jndiEnv) {

            if (jndiEnv == null || jndiEnv.isEmpty()) {
                return null;
            }
            try {

                if (log.isDebugEnabled()) {
                    log.debug("Initiating a Naming context with JNDI " +
                            "environment jndiProperties :  " + jndiEnv);
                }

                return new InitialContext(jndiEnv);

            } catch (NamingException e) {
                throw new SynapseCommonsException("Error creating a InitialContext" +
                        " with JNDI env jndiProperties : " + jndiEnv, log);
            }
        }

        private boolean isValid(Properties dsProperties) {

            String dataSources = MiscellaneousUtil.getProperty(dsProperties,
                    DataSourceConstants.PROP_SYNAPSE_PREFIX_DS, null);

            if (dataSources != null && !"".equals(dataSources)) {
                String[] dataSourcesNames = dataSources.split(",");
                return !(dataSourcesNames == null || dataSourcesNames.length == 0);
            }

            return false;
        }
    }