DBManager.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2024 Link.it srl (https://link.it). 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */



package org.openspcoop2.pdd.config;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Enumeration;

import javax.naming.RefAddr;
import javax.sql.DataSource;

import org.openspcoop2.core.commons.CoreException;
import org.openspcoop2.core.commons.IMonitoraggioRisorsa;
import org.openspcoop2.core.constants.CostantiDB;
import org.openspcoop2.core.id.IDSoggetto;
import org.openspcoop2.pdd.logger.MsgDiagnostico;
import org.openspcoop2.pdd.logger.OpenSPCoop2Logger;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.date.DateManager;
import org.openspcoop2.utils.date.DateUtils;
import org.openspcoop2.utils.id.UniqueIdentifierException;
import org.openspcoop2.utils.resources.GestoreJNDI;
import org.slf4j.Logger;


/**
 * Contiene la gestione delle connessioni ad un Database.
 * Il nome della risorsa JNDI da cui e' possibili attingere connessioni verso il Database, 
 * viene selezionato attraverso le impostazioni lette dal file 'govway.properties'
 * e gestite attraverso l'utilizzo della classe  {@link org.openspcoop2.pdd.config.OpenSPCoop2Properties}.
 *
 *
 * @author Poli Andrea (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */

public class DBManager implements IMonitoraggioRisorsa {

	private static final String ID_MODULO = "DBManager";
	
	/** DBManager */
	private static DBManager manager = null;
	/** TransactionIsolation level */
	private static int transactionIsolationLevel = -1;
	public static void setTransactionIsolationLevel(int transactionIsolationLevel) {
		DBManager.transactionIsolationLevel = transactionIsolationLevel;
	}

	/** NomeJNDIC DataSource dove attingere connessioni */
	private String dataSourceJndiName = null;
	/** DataSource dove attingere connessioni */
	private DataSource dataSource = null;
	/** MsgDiagnostico */
	private MsgDiagnostico msgDiag = null;

	/** Informazione sui proprietari che hanno richiesto una connessione */
	private static java.util.concurrent.ConcurrentHashMap<String,Resource> risorseInGestione = new java.util.concurrent.ConcurrentHashMap<>();
	
	/** Informazioni sui check */
	private static boolean getConnectionCheckAutoCommitDisabled;
	private static boolean getConnectionCheckTransactionIsolation;
	private static int getConnectionCheckTransactionIsolationExpected;
	
	/** Stato di inizializzazione del manager */
	private static boolean initialized = false;

	public static String[] getStatoRisorse() {	
		return getStatoRisorse(DBManager.risorseInGestione);
	}
	public static String[] getStatoRisorse(java.util.concurrent.ConcurrentMap<String,Resource> risorseInGestione) {	
		String[] sNull = null;
		Object[] o = risorseInGestione.values().toArray(new Resource[0]);
		if(! (o instanceof Resource[]))
			return sNull;
		Resource[] resources = (Resource[]) o;
		if(resources.length<=0)
			return sNull;
	
		String [] r = new String[resources.length];
		for(int i=0; i<resources.length; i++){
			Resource rr = resources[i];
			r[i] = rr.getIdentificativoPorta()+"."+rr.getModuloFunzionale();
			if(rr.getIdTransazione()!=null){
				r[i] = r[i] +"."+rr.getIdTransazione();
			}
			SimpleDateFormat dateformat = DateUtils.getSimpleDateFormatMs();
			r[i] = r[i] +" ("+dateformat.format(rr.getDate())+")";
		}
		return r;
		
	}


	/**
	 * Viene chiamato in causa per istanziare il datasource
	 * @throws SQLException 
	 *
	 * 
	 */
	private DBManager(String jndiName,java.util.Properties context) throws UtilsException, SQLException {

		this.msgDiag = MsgDiagnostico.newInstance(ID_MODULO);

		GestoreJNDI jndi = new GestoreJNDI(context);
		Object oSearch = jndi.lookup(jndiName);
		if(oSearch==null){
			throw new UtilsException("Lookup jndiResource ["+jndiName+"] not found");
		}
		try{
			this.dataSourceJndiName = jndiName;
			this.dataSource = (DataSource) oSearch;
		}catch(Throwable t){
			StringBuilder bf = new StringBuilder();
			if(oSearch instanceof javax.naming.Reference){
				javax.naming.Reference r = (javax.naming.Reference) oSearch;
				bf.append(" (Factory=");
				bf.append(r.getFactoryClassName());
				bf.append(" FactoryLocation=");
				bf.append(r.getFactoryClassLocation());
				Enumeration<RefAddr> enR = r.getAll();
				if(enR!=null){
					while (enR.hasMoreElements()) {
						RefAddr refAddr = enR.nextElement();
						bf.append(" [").
							append("type=").
							append(refAddr.getType()).
							append(" content=").
							append(refAddr.getContent()).
							append("]");
					}
				}
				bf.append(")");
			}
			throw new UtilsException("lookup failed (object class: "+oSearch.getClass().getName()+")"+bf.toString()+": "+t.getMessage(),t);
		}
		/**this.jndiName = jndiName;*/
		
		// Prelevo livello di transaction isolation
		try(java.sql.Connection connectionTest = this.dataSource.getConnection();){
			DBManager.setTransactionIsolationLevel(connectionTest.getTransactionIsolation());
		}	    

	}
	
	
    public static boolean isInitialized() {
    	return DBManager.initialized;
    }

    private static void setInitialized(boolean initialized) {
        DBManager.initialized = initialized;        
    }



	/**
	 * Il Metodo si occupa di inizializzare il propertiesReader del QueueManager 
	 *
	 * @param jndiName Nome JNDI del Datasource
	 * @param context Contesto JNDI da utilizzare
	 * @throws SQLException 
	 * @throws UtilsException 
	 * 
	 */
	public static void initialize(String jndiName,java.util.Properties context) throws UtilsException, SQLException {
		DBManager.manager = new DBManager(jndiName,context);	
		DBManager.setInitialized(true);
		
		OpenSPCoop2Properties properties = OpenSPCoop2Properties.getInstance();
		if(properties!=null) {
			DBManager.getConnectionCheckAutoCommitDisabled = properties.isDataSourceGetConnectionCheckAutoCommitDisabled();
			DBManager.getConnectionCheckTransactionIsolation = properties.isDataSourceGetConnectionCheckTransactionIsolationLevel();
			if(DBManager.getConnectionCheckTransactionIsolation) {
				DBManager.getConnectionCheckTransactionIsolationExpected = properties.getDataSourceGetConnectionCheckTransactionIsolationLevelExpected();
			}
		}
	}

	/**
	 * Ritorna l'istanza di questo DBManager
	 *
	 * @return Istanza di DBManager
	 * 
	 */
	public static DBManager getInstance(){
		if(DBManager.manager==null) {
			// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED': l'istanza viene creata allo startup
			synchronized (DBManager.class) {
				return DBManager.manager;
			}
		}
		return DBManager.manager;
	}

	/**
	 * Ritorna il livello di isolamento di default del DataSource
	 *
	 * @return livello di isolamento di default del DataSource
	 * 
	 */
	public static int getTransactionIsolationLevel(){
		return DBManager.transactionIsolationLevel;
	}







	/**
	 * Viene chiamato in causa per ottenere una connessione al DB
	 *
	 * @param idPDD Identificatore della porta di dominio.
	 * @param modulo Modulo che richiede una connessione al database.
	 * @return java.sql.Connection aperta sul database.
	 * 
	 */
	public Resource getResource(IDSoggetto idPDD,String modulo,String idTransazione) throws UtilsException {
		return this.getResource(idPDD, modulo, idTransazione, true);
	}
	public Resource getResource(IDSoggetto idPDD,String modulo,String idTransazione, boolean logError) throws UtilsException {

		if(this.dataSource == null)
			throw new UtilsException("Datasource non istanziato");

		Resource risorsa = null;
		try {
			risorsa = DBManager.buildResource(this.dataSourceJndiName, this.dataSource, idPDD, modulo, idTransazione);
				
			DBManager.risorseInGestione.put(risorsa.getId(), risorsa);
			
			/**if(this.dataSource instanceof SharedPoolDataSource)
				System.out.println("IDLE["+((SharedPoolDataSource)this.dataSource).getNumIdle()+"] ACTIVE["+((SharedPoolDataSource)this.dataSource).getNumActive()+"]");*/
						
		}
		catch(Exception e) {
			if(logError) {
				this.msgDiag.aggiornaFiltri();
				this.msgDiag.setDominio(idPDD);
				this.msgDiag.setFunzione("DBManager."+modulo);
				this.msgDiag.logFatalError(e, "Richiesta connessione al datasource");
			}
			throw new UtilsException(e.getMessage(),e);
		}
		
		return risorsa;
	}

	public static Resource buildResource(String dataSourceJndiName, DataSource dataSource, IDSoggetto idPDD,String modulo,String idTransazione) throws UtilsException, SQLException, UniqueIdentifierException {
		if(dataSourceJndiName!=null) {
			// log
		}
		/**System.out.println("### buildResource ["+dataSourceJndiName+"] modulo:"+modulo+" idTransazione:"+idTransazione+" ...");*/
		Connection connectionDB = dataSource.getConnection();
		checkConnection(connectionDB);
		/**System.out.println("### buildResource ["+dataSourceJndiName+"] modulo:"+modulo+" idTransazione:"+idTransazione+" OK");*/
		return buildResource("DBRuntimeManager", connectionDB, idPDD, modulo, idTransazione);
	}
	private static void checkConnection(Connection connectionDB) throws UtilsException {
		if(connectionDB==null)
			throw new UtilsException("is null");
	}
	public static Resource buildResource(String managerId, Connection connectionDB, IDSoggetto idPDD,String modulo,String idTransazione) throws SQLException, UniqueIdentifierException {
		
		/**if(modulo==null || !modulo.contains("Timer") || modulo.contains("TimerConsegnaContenutiApplicativi")) {
			System.out.println("### buildResource managerId["+managerId+"] modulo:"+modulo+" idTransazione:"+idTransazione+" ...");
		}*/
		
		if(connectionDB!=null) {
			if(DBManager.getConnectionCheckAutoCommitDisabled &&
				!connectionDB.getAutoCommit()) {
				Logger log = OpenSPCoop2Logger.getLoggerOpenSPCoopResources(); 
				if(log!=null) {
					String msg = getPrefixLog(managerId, idPDD,modulo,idTransazione)+" Connessione ottenuta possiede autoCommit enabled ?";
					log.error(msg);
				}
			}
			if(DBManager.getConnectionCheckTransactionIsolation &&
				DBManager.getConnectionCheckTransactionIsolationExpected != connectionDB.getTransactionIsolation()) {
				Logger log = OpenSPCoop2Logger.getLoggerOpenSPCoopResources(); 
				if(log!=null) {
					String msg = getPrefixLog(managerId, idPDD,modulo,idTransazione)+" Connessione ottenuta possiede un transaction isolation level '"+connectionDB.getTransactionIsolation()+"' differente da quello atteso '"+DBManager.getConnectionCheckTransactionIsolationExpected+"'";
					log.error(msg);
				}
			}
		}
		
		Resource risorsa = new Resource();
		String idUnivoco = Resource.generaIdentificatoreUnivoco(idPDD, modulo);
		risorsa.setId(idUnivoco);
		risorsa.setDate(DateManager.getDate());
		risorsa.setIdentificativoPorta(idPDD);
		risorsa.setModuloFunzionale(modulo);
		risorsa.setResource(connectionDB);
		risorsa.setResourceType(Connection.class.getName());
		risorsa.setIdTransazione(idTransazione);
		
		return risorsa;

	}
	private static String getPrefixLog(String managerId, IDSoggetto idPDD,String modulo,String idTransazione) {
		StringBuilder sb = new StringBuilder("");
		if(managerId!=null) {
			if(sb.length()>0) {
				sb.append(" ");
			}
			sb.append("[").append(managerId).append("]");
		}
		if(idTransazione!=null) {
			if(sb.length()>0) {
				sb.append(" ");
			}
			sb.append("<").append(idTransazione).append(">");
		}
		if(modulo!=null) {
			if(sb.length()>0) {
				sb.append(" ");
			}
			sb.append("modulo:").append(modulo);
		}
		if(idPDD!=null) {
			if(sb.length()>0) {
				sb.append(" ");
			}
			sb.append("soggetto:").append(idPDD.toString());
		}
		return sb.toString();
	}




	/**
	 * Viene chiamato in causa per rilasciare una connessione al DB, effettuando precedentemente un commit
	 *
	 * @param idPDD Identificatore della porta di dominio.
	 * @param modulo Modulo che richiede il rilascio di una connessione al database.
	 * @param resource Connessione da rilasciare.
	 * 
	 */
	public void releaseResource(IDSoggetto idPDD,String modulo,Resource resource){
		this.releaseResource(idPDD, modulo, resource, true);
	}
	public void releaseResource(IDSoggetto idPDD,String modulo,Resource resource, boolean logError){
		try {
			if(resource!=null){
				
				if(resource.getResource()!=null){
					Connection connectionDB = (Connection) resource.getResource();
					if(connectionDB != null && (!connectionDB.isClosed())){
						connectionDB.close();
					}
				}
				if(DBManager.risorseInGestione.containsKey(resource.getId()))
					DBManager.risorseInGestione.remove(resource.getId());
				
				/**System.out.println("### releaseResource ["+this.dataSourceJndiName+"] modulo:"+modulo+" idTransazione:"+resource.getId()+" OK");
				if(this.dataSource instanceof SharedPoolDataSource)
					System.out.println("CLOSE IDLE["+((SharedPoolDataSource)this.dataSource).getNumIdle()+"] ACTIVE["+((SharedPoolDataSource)this.dataSource).getNumActive()+"]");*/
									
			}

		}
		catch(SQLException e) {
			if(logError) {
				this.msgDiag.aggiornaFiltri();
				this.msgDiag.setDominio(idPDD);
				this.msgDiag.setFunzione("DBManager."+modulo);
				this.msgDiag.logFatalError(e, "Rilasciata connessione al datasource");
			}
		}
	}

	
	
	/**
	 * Metodo che verica la connessione ad una risorsa.
	 * Se la connessione non e' presente, viene lanciata una eccezione che contiene il motivo della mancata connessione
	 * 
	 * @throws DriverException eccezione che contiene il motivo della mancata connessione
	 */
	@Override
	public void isAlive() throws CoreException{
		// Verifico la connessione
		Resource resource = null;
		Statement stmtTest = null;
		IDSoggetto idSoggettAlive = new IDSoggetto();
		idSoggettAlive.setCodicePorta(ID_MODULO);
		idSoggettAlive.setTipo(ID_MODULO);
		idSoggettAlive.setNome(ID_MODULO);
		try {
			resource = getResource(idSoggettAlive);
			if(resource == null)
				throw new CoreException("Resource is null");
			if(resource.getResource() == null)
				throw new CoreException("Connessione is null");
			Connection con = (Connection) resource.getResource();
			// test:
			stmtTest = con.createStatement();
			stmtTest.execute("SELECT * from "+CostantiDB.DB_INFO);
					
		} catch (Exception e) {
			throw new CoreException("Connessione al database GovWay non disponibile: "+e.getMessage(),e);

		}finally{
			try{
				if(stmtTest!=null)
					stmtTest.close();
			}catch(Exception e){
				// close
			}
			try{
				this.releaseResource(idSoggettAlive, "CheckIsAlive", resource);
			}catch(Exception e){
				// close
			}
		}
	}
	private Resource getResource(IDSoggetto idSoggettAlive) throws CoreException {
		try{
			return this.getResource(idSoggettAlive, "CheckIsAlive", null,
					false); // verra' loggato nel servizio di check, altrimenti ad ogni test viene registrato l'errore
		}catch(Exception e){
			throw new CoreException(e.getMessage(),e);
		}
	}


	public DataSource getDataSource() {
		return this.dataSource;
	}
}