PoolManager.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.util.pool;

import java.io.InputStream;
import java.sql.Connection;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import fr.paris.lutece.portal.service.init.LuteceInitException;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.pool.service.ConnectionService;
import fr.paris.lutece.util.pool.service.LuteceConnectionService;

/**
 * This class can manages a set of database connections pools. It's implemented as a singleton. It provides methods to get or release a connection from a given
 * pool.
 */
public final class PoolManager
{
    private static final String LOGGER_NAME = "lutece.pool";
    private static PoolManager _instance;
    private Logger _logger;
    private Map<String, ConnectionService> _pools = new HashMap<>( );

    /**
     * Creates a new PoolManager object.
     *
     *
     * @param isDbProperties
     *            A properties file containing pools parameters.
     * @throws LuteceInitException
     *             If any error occured
     */
    private PoolManager( InputStream isDbProperties ) throws LuteceInitException
    {
        init( isDbProperties );
    }

    /**
     * This method returns the unique instance of the PoolManager.
     *
     * @return The unique instance of Poolmanager.
     * @param isDbProperties
     *            An InputStream on a db.properties File to initialiaze the pool if it's not already created.
     * @throws LuteceInitException
     *             If any error occured
     */
    public static synchronized PoolManager getInstance( InputStream isDbProperties ) throws LuteceInitException
    {
        if ( _instance == null )
        {
            _instance = new PoolManager( isDbProperties );
        }

        return _instance;
    }

    /**
     * Initializes pools with parameters defined in a db.properties File.
     *
     * @param is
     *            An InputStream on a db.properties File.
     * @throws LuteceInitException
     *             If any error occured
     */
    private void init( InputStream is ) throws LuteceInitException
    {
        _logger = LogManager.getLogger( LOGGER_NAME );

        Properties dbProps = new Properties( );

        try
        {
            dbProps.load( is );
        }
        catch( Exception e )
        {
            throw new LuteceInitException( "Can't read the properties file. Make sure db.properties is in the CLASSPATH", e );
        }
        
        overrideProperties(dbProps);       
        createPools( dbProps );
    }

    /**
     * Override properties with the api config
     * @param dbProps the database properties 
     */
	private void overrideProperties(Properties dbProps) {
		Enumeration propertiesName=  dbProps.propertyNames();
        while (propertiesName.hasMoreElements()) {
        	 String key = (String) propertiesName.nextElement();
        	 String value= AppPropertiesService.getProperty(key);
        	 if( value != null ) {
        		 dbProps.put(key, value);
        	 }
        }
	}

    /**
     * Creates all pools defined in a properties file.
     *
     * @param props
     *            A properties file containing pools parameters.
     * @throws LuteceInitException
     *             If any error occured
     */
    private void createPools( Properties props ) throws LuteceInitException
    {
        Enumeration propNames = props.propertyNames( );
        String strPoolName = "";

        Map<String, Hashtable<String, String>> htPools = new HashMap<>( );

        while ( propNames.hasMoreElements( ) )
        {
            String name = (String) propNames.nextElement( );

            try
            {
                strPoolName = name.substring( 0, name.lastIndexOf( '.' ) );

                // tests if the pool has yet somme of its porperties stored in the hatsable
                Hashtable<String, String> htParamsPool = htPools.computeIfAbsent( strPoolName, s -> new Hashtable<>( ) );
                htParamsPool.put( name, props.getProperty( name ) );
                htPools.put( strPoolName, htParamsPool );

                _logger.debug( "property {}", name );
                _logger.debug( "pool name {}", strPoolName );
            }
            catch( Exception e )
            {
                throw new LuteceInitException( "Invalid initialization of the pools. Problem encoutered with the property :  " + name, e );
            }
        }

        for ( Entry<String, Hashtable<String, String>> entry : htPools.entrySet( ) )
        {
            String key = entry.getKey( );
            try
            {
                Hashtable<String, String> htParamsPool = htPools.get( key );
                ConnectionService cs = getConnectionService( htParamsPool, key );

                if ( cs != null )
                {
                    cs.setPoolName( key );
                    cs.setLogger( _logger );
                    cs.init( htParamsPool );
                    _pools.put( key, cs );
                }
            }
            catch( Exception e )
            {
                throw new LuteceInitException( "Exception when getting the pool '" + key + "'. Please check your '/WEB-INF/conf/db.properties' file.", e );
            }
        }
    }

    private ConnectionService getConnectionService( Map<String, String> htParamsPool, String key ) throws LuteceInitException
    {
        ConnectionService cs = null;

        try
        {
            String strConnectionService = htParamsPool.get( key + ".poolservice" );

            cs = (ConnectionService) Class.forName( strConnectionService ).newInstance( );
        }
        catch( NullPointerException nullEx )
        {
            cs = new LuteceConnectionService( );
        }
        catch( Exception e )
        {
            throw new LuteceInitException( "Exception when getting the property poolservice", e );
        }
        return cs;
    }

    /**
     * Returns an available connection from the pool.
     *
     * @param strPoolName
     *            The pool name
     * @return A connection
     */
    public Connection getConnection( String strPoolName )
    {
        Connection conn = null;
        ConnectionService pool = _pools.get( strPoolName );

        if ( pool != null )
        {
            conn = pool.getConnection( );
        }

        return conn;
    }

    /**
     * Returns a connection to pool.
     *
     * @param strPoolName
     *            Pool's name
     * @param con
     *            A released connection
     */
    public void freeConnection( String strPoolName, Connection con )
    {
        ConnectionService cs = _pools.get( strPoolName );

        if ( cs != null )
        {
            cs.freeConnection( con );
        }
    }

    /**
     * Releases all connections from all the pool.
     */
    public synchronized void release( )
    {
        for ( ConnectionService pool : _pools.values( ) )
        {
            pool.release( );
        }
    }

    /**
     * Returns all pools available
     * 
     * @return The list of available pools
     */
    public Collection<ConnectionService> getPools( )
    {
        return _pools.values( );
    }

    /**
     * Returns pool's infos (currently opened connections)
     * 
     * @return The pool's infos
     */
    public ReferenceList getPoolsInfos( )
    {
        ReferenceList listPoolsInfos = new ReferenceList( );
        Collection<ConnectionService> listPools = getPools( );

        for ( ConnectionService cs : listPools )
        {
            String strCurrentConnections = ( cs.getCurrentConnections( ) == ConnectionService.INFO_NOT_AVAILABLE ) ? "-" : ( "" + cs.getCurrentConnections( ) );
            String strMaxConnections = ( cs.getMaxConnections( ) == ConnectionService.INFO_NOT_AVAILABLE ) ? "-" : ( "" + cs.getMaxConnections( ) );
            listPoolsInfos.addItem( cs.getPoolName( ), strCurrentConnections + " / " + strMaxConnections + " (" + cs.getPoolProvider( ) + ")" );
        }

        return listPoolsInfos;
    }

    /**
     * Returns the datasource for a given pool name
     * 
     * @param strPoolName
     *            The Pool name
     * @return A data source object
     */
    public DataSource getDataSource( String strPoolName )
    {
        return _pools.get( strPoolName ).getDataSource( );
    }
}