/*
 * PoolConexoes.java
 *
 * Created on 19 de Novembro de 2001, 11:03
 */

package sac.persistencia;

import java.util.Properties;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.io.IOException;
import java.io.InputStream;

/**
 * Classe que implementa um pool de conexões (para qualquer BD com interface
 * JDBC).
 * Esta classe necessita de um arquivo de configurações chamado <code>db.properties</code>,
 * que deve estar no <code>CLASSPATH</code>.
 * O arquivo de configurações deve ter as seguintes propriedades:<br><br>
 * <code>drivers</code>: nome das classes dos drivers JDBC que devem ser 
 * carregados, separados por espaço. (Obrigatório)<br><br>
 * <code>url</code>: Url do banco. Pode ser no formato que apenas especifica o banco
 * ou no formato que já especifica também o usuário e password.<br><br>
 * <code>user</code>: login do usuário do banco. Necessário se a url não especificá-lo.<br><br>
 * <code>password</code>: password do usuário do banco. Necessário se a url não especificar o usuário e password.<br><br>
 * <code>maxconns</code>: número máximo de conexões que podem ser abertas com o banco de dados.
 * É altamente recomendável que seja especificado. Caso contrário, o pool tentará abrir uma nova
 * conexão, sempre que for necessário (sem limite máximo).<br><br>
 * <code>initconns</code>: número inicial de conexões que serão abertas com o banco de dados.
 * Caso não seja especificado, nenhuma conexão é aberta no início (abertura apenas sob demanda).<br><br>
 * <code>timeout</code>: tempo máximo (em segundos) para ficar esperando por uma conexão disponível, antes de 
 * desistir (levantar uma SQLException). Caso não seja especificado, é assumido o
 * valor default de 5 segundos.<br>
 * Exemplo de arquivo db.properties:<br><br>
 * <code>
 * drivers=org.srmq.jdbc.ImaginaryJDBCClass<br>
 * url=jdbc:SQLDB:SRMQDB<br>
 * user=srmq<br>
 * password=secret<br>
 * maxconns=10<br>
 * initcons=3
 * </code>
 * <br><br>
 * No exemplo acima não foi especificado timeout, portanto será usado 5 segundos.
 * @author  srmq
 */
public class PoolConexoes {

    private static PoolConexoes instancia = null;
    
    /** Arquivo de configurações do BD */
    private static final String CONFIG_FILE = "/db.properties";
    
    private List drivers = new ArrayList();    
    
    private String url;
    
    private String user;
    
    private String password;
    
    private int maxConns;
    
    private int timeOut;
    
    private List conexoesLivres = new ArrayList();
    
    private int conexoesEmUso = 0;
    
    /** Retorna a instância única do Pool de Conexões
     * @return a instância de <code>PoolConexoes</code>
     */
    public static synchronized PoolConexoes getInstancia() {
        if (instancia == null) {
            instancia = new PoolConexoes();
        }
        return instancia;
    }
    
    
    /** Creates new PoolConexoes */
    private PoolConexoes() {
        Properties props = lerPropriedades();
        carregarDrivers(props);
        inicializarPool(props);
    }
    
    private Properties lerPropriedades() {
        InputStream is = getClass().getResourceAsStream(CONFIG_FILE);
        Properties dbProps = new Properties();
	try {
		dbProps.load(is);
	} catch (IOException ex) {
		ex.printStackTrace();
                String msg = "Não foi possível carregar o arquivo de " +
                        "propriedades da configuração do banco de dados";
                System.err.println(msg);
                throw new IllegalStateException(msg);
	}
        return dbProps;        
    }
    
    private void carregarDrivers(Properties props) {
        String driverClasses = props.getProperty("drivers");
        StringTokenizer st = new StringTokenizer(driverClasses);
        while (st.hasMoreElements()) {
            String driverClassName = st.nextToken().trim();
            try {
                Driver driver = (Driver) Class.forName(driverClassName).newInstance();
                DriverManager.registerDriver(driver);
                drivers.add(driver);
                System.out.println("Registrado driver JDBC: " + driverClassName);
            } catch (Exception e) {
                System.err.println("Impossível registrar driver JDBC: " + driverClassName);
                e.printStackTrace();
            }
        }
    }
    
    private void inicializarPool(Properties props) {
        String url = props.getProperty("url");
        if (url == null) {
            String msg = "A URL do banco de dados não foi especificada";
            System.err.println(msg);
            throw new IllegalStateException(msg);
        }
        String user = props.getProperty("user");
        String password = props.getProperty("password");
        String maxConns = props.getProperty("maxconns", "0");
        int max;
        try {
            max = Integer.parseInt(maxConns);
        } catch (NumberFormatException ex) {
            System.err.println("Valor inválido para o número máximo" + 
                    "de conexões. Assumindo \"ilimitado\"");
            max = 0;
        }
        String initConns = props.getProperty("initconns", "0");
        int initc;
        try {
            initc = Integer.parseInt(initConns);
        } catch (NumberFormatException ex) {
            System.err.println("Valor inválido para o número inicial" + 
                    "de conexões. Assumindo zero");
            initc = 0;
        }
        String conTimeOut = props.getProperty("timeout", "5");
        int timeOut;
        try {
            timeOut = Integer.parseInt(conTimeOut);
        } catch (NumberFormatException ex) {
            System.err.println("Valor inválido para o timeout" + 
                    ". Assumindo 5 segundos.");
           timeOut = 5;
        }
        criarPool(url, user, password, max, initc, timeOut);
    }

    private void criarPool(String url, String user, String password,
            int maxConns, int initConns, int timeOut) {
    
        this.url = url;
        this.user = user;
        this.password = password;
        this.maxConns = (maxConns >= 0) ? maxConns : 0;
        this.timeOut = (timeOut > 0) ? timeOut : 5;
        
        for (int i = 0; i < initConns; i++) {
            try {
                Connection pc = novaConexao();
                conexoesLivres.add(pc);
            } catch (SQLException e) { }
        }
	System.out.println("Pool criado");
	String lf = System.getProperty("line.separator");
	System.out.println(lf +
            " url=" + this.url + lf +
            " user=" + this.user + lf +
            " password=" + this.password + lf +
            " initconns=" + initConns + lf +
            " maxconns=" + this.maxConns + lf +
            " logintimeout=" + this.timeOut);
	System.out.println(getStats());
        
    }
    
    private String getStats() {
        return "Total de conexões: " + 
             (conexoesLivres.size() + conexoesEmUso) +
             " Disponíveis: " + conexoesLivres.size() +
             " Em uso: " + conexoesEmUso;
    }                   
    
    private Connection novaConexao() throws SQLException {
        Connection conn = null;
        if (this.user == null) {
            conn = DriverManager.getConnection(this.url);
        }
        else {
            conn = DriverManager.getConnection(this.url, this.user, this.password);
        }
        System.out.println("Aberta uma nova conexão");
        return conn;
    }
    
    synchronized void wrapperClosed(Connection conn) {
        // Coloca a conexão liberada no final do Connection pool.
        conexoesLivres.add(conn);
        conexoesEmUso--;
        System.out.println("Conexao retornada ao pool");
        System.out.println(getStats());
        notifyAll();
    }
    
    /** Devolve uma conexão ao pool após a sua utilização. Este é apenas um
     *  método de conveniência. O usuário pode usar diretamente o método 
     *  <code>close()</code> da conexão, o efeito será o mesmo.
     *  @param conn Conexão a ser liberada. 
     */
    public void liberarConexao(Connection conn) throws SQLException {
        if (!conn.isClosed()) {
            conn.close();
        }
    }
    
    /** Retorna uma <code>Connection</code> do pool.
     * @return uma <code>Connection</code> para ser usada pelo usuário.
     * @throws SQLException caso ocorra algum problema para obter uma conexão
     * com o BD, ou todas as conexões estejam alocadas e nenhuma seja liberada
     * dentro do <em>timeout</em> configurado.
     */
    public Connection obterConexao() throws SQLException {
        System.out.println("Recebido pedido de conexao");
	try {
            Connection conn = obterConexao(timeOut * 1000);
            return new ConnectionWrapper(conn, this);
	} catch (SQLException ex) {
		 System.out.println("Exceção ao obter conexão");
		 throw ex;
	}
   }               

    /** 
     * Fornece uma conexão do pool ou uma nova (caso o limite máximo não tiver sido
     * atingido, mas todas as conexões já criadas estão alocadas).
     * Espera caso contrário.
     */
    private synchronized Connection obterConexao(long timeout) throws SQLException {
        long startTime = System.currentTimeMillis();
	long remaining = timeout;
	Connection conn = null;
	while ((conn = obterConexaoPooled()) == null) {
            try {
                System.out.println("Esperando por conexão. Timeout=" + remaining);
                wait(remaining);
            } catch (InterruptedException ex) { }
            remaining = timeout - (System.currentTimeMillis() - startTime);
            if (remaining <= 0) {
                // Timeout expirou
                System.out.println("Timeout esperando por conexáo");
                throw new SQLException("Timeout em obterConexao()");
            }
        }

	// Checa se a conexão continua OK
	if (!isConexaoOK(conn)) {
            // Estava bichada, tentar novamente com o tempo restante.
            System.err.println("Conexão bichada removida do pool");
            return obterConexao(remaining);
	}
	conexoesEmUso++;
	System.out.println("Conexáo cedida pelo pool");
	System.out.println(getStats());
	return conn;
    }

    private Connection obterConexaoPooled() throws SQLException {
        Connection conn = null;
        if (conexoesLivres.size() > 0) {
            // Pega a primeira conexão da Lista
            // para obter um comportamento round-robin.
            conn = (Connection) conexoesLivres.remove(0);
	} else if (this.maxConns == 0 || this.conexoesEmUso < this.maxConns) {
		 conn = novaConexao();
        }
        return conn;
    }                        
    
    private boolean isConexaoOK(Connection conn) {
        Statement testStmt = null;
        try {
            if (!conn.isClosed()) {
                // Tenta criar um statement para ver se a conexao realmente está
                // viva
                testStmt = conn.createStatement();
                testStmt.close();
            } else {
                return false;
            }
        }
        catch (SQLException e) {
            if (testStmt != null) {
                try {
                    testStmt.close();
                } catch (SQLException se) { }
            }
            System.err.println("Conexão pooled estava bichada");
            return false;
        }
        return true;
    }
    
    protected void finalize() {
        Iterator conexoes = conexoesLivres.iterator();
        while (conexoes.hasNext()) {
            Connection con = (Connection) conexoes.next();
            conexoes.remove();
            try {
                con.close();
                System.out.println("Conexão fechada");
            }
            catch (SQLException e) {
                System.err.println("Não foi possível fechar uma conexão (SQLException)");
            }
        }
    }
}
