DigestServiceParamsDriver.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2025 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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;

import org.openspcoop2.core.config.driver.DriverConfigurazioneException;
import org.openspcoop2.core.config.driver.db.DriverConfigurazioneDB;
import org.openspcoop2.core.constants.CostantiDB;
import org.openspcoop2.core.id.IDServizio;
import org.openspcoop2.pdd.core.GestoreMessaggi;
import org.openspcoop2.utils.SemaphoreLock;
import org.openspcoop2.utils.TipiDatabase;
import org.openspcoop2.utils.Utilities;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.digest.DigestType;
import org.openspcoop2.utils.id.serial.InfoStatistics;
import org.openspcoop2.utils.semaphore.Semaphore;
import org.openspcoop2.utils.semaphore.SemaphoreConfiguration;
import org.openspcoop2.utils.semaphore.SemaphoreMapping;
import org.openspcoop2.utils.sql.ISQLQueryObject;
import org.openspcoop2.utils.sql.SQLObjectFactory;
import org.openspcoop2.utils.sql.SQLQueryObjectException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DigestServiceParamsDriver
 *
 * @author Burlon Tommaso (tommaso.burlon@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class DigestServiceParamsDriver {

	private static final String DB_LOCK_ID = "ServiziDigestParamsUpdate";
	private static final org.openspcoop2.utils.Semaphore THREAD_LOCK = new org.openspcoop2.utils.Semaphore("DigestServiceParamsDriver-threadLock");
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	private DriverConfigurazioneDB driverConfigurazioneDB;

	private String getKey(IDServizio idServizio, Long serialNumber) {
		return "DigestServiceParams" + idServizio.toString() + "-" + serialNumber;
	}
	
	private Long getIdServizio(Connection conn, String tipoDB, IDServizio idServizio) throws SQLQueryObjectException, SQLException {
		ISQLQueryObject query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
		query.addFromTable(CostantiDB.SOGGETTI);
		query.addSelectField("id");
		query.setANDLogicOperator(true);
		query.addWhereCondition(CostantiDB.SOGGETTI_COLUMN_NOME_SOGGETTO + "= ?");
		query.addWhereCondition(CostantiDB.SOGGETTI_COLUMN_TIPO_SOGGETTO + "= ?");
		
		Long idSoggetto = null;
		try( PreparedStatement stmt = conn.prepareStatement(query.createSQLQuery())) {
			int index = 1;
			stmt.setString(index++, idServizio.getSoggettoErogatore().getNome());
			stmt.setString(index++, idServizio.getSoggettoErogatore().getTipo());
			
			try (ResultSet rs = stmt.executeQuery()) {
				if (rs.next()) {
					idSoggetto = rs.getLong("id");
				}
			}
		}
		
		if (idSoggetto == null)
			return null;
		
		query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
		query.addFromTable(CostantiDB.SERVIZI);
		query.addSelectField("id");
		query.setANDLogicOperator(true);
		query.addWhereCondition(CostantiDB.SERVIZI_COLUMN_NOME_SERVIZIO + "= ?");
		query.addWhereCondition(CostantiDB.SERVIZI_COLUMN_TIPO_SERVIZIO + "= ?");
		query.addWhereCondition(CostantiDB.SERVIZI_COLUMN_VERSIONE_SERVIZIO + "= ?");
		query.addWhereCondition(CostantiDB.SERVIZI_COLUMN_ID_SOGGETTO_REF + "= ?");
		try( PreparedStatement stmt = conn.prepareStatement(query.createSQLQuery())) {
			int index = 1;
			stmt.setString(index++, idServizio.getNome());
			stmt.setString(index++, idServizio.getTipo());
			stmt.setInt(index++, idServizio.getVersione());
			stmt.setLong(index++, idSoggetto);
			
			try (ResultSet rs = stmt.executeQuery()) {
				if (rs.next()) {
					return rs.getLong("id");
				}
			}
		}
		
		return null;
	}
	
	private DigestServiceParams paramsFromResultSet(IDServizio idServizio, ResultSet rs) throws SQLException {
		DigestServiceParams params = new DigestServiceParams();
		params.setIdServizio(idServizio);
		params.setSerialNumber(rs.getLong(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_SERIAL_NUMBER));
		params.setDataRegistrazione(rs.getTimestamp(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE).toInstant());
		params.setDurata(rs.getInt(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_PERIOD));
		params.setSeed(rs.getString(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_SEED).getBytes());
		params.setDigestAlgorithm(DigestType.valueOf(rs.getString(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ALGORITHM)));
		return params;
	}
	
	public DigestServiceParamsDriver(DriverConfigurazioneDB driverConfigurazioneDB) {
		this.driverConfigurazioneDB = driverConfigurazioneDB;
	}
	
	/**
	 * Rimuove tutte le informazioni crittografiche per la generazione dei digest per un determinato
	 * servizio.
	 * @param idServizio servizio di cui rimuovere le informazioni crittografiche
	 * @return
	 * @throws DriverConfigurazioneException nel caso la rimozione non avvenga correttamente
	 */
	public boolean removeEntries(IDServizio idServizio) throws DriverConfigurazioneException {
		Connection conn = null;
		String tipoDB = this.driverConfigurazioneDB.getTipoDB();
		try { 
			conn = this.driverConfigurazioneDB.getConnection("removeEntries");
			
			// ottengo l'id di riferimento del servizio
			Long idServizioRef = this.getIdServizio(conn, tipoDB, idServizio);
			if (idServizioRef == null)
				return false;
			
			ISQLQueryObject query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
			query.addDeleteTable(CostantiDB.SERVIZI_DIGEST_PARAMS);
			query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ID_SERVIZIO_REF + "= ?");
			
			try (PreparedStatement stmt = conn.prepareStatement(query.createSQLDelete())) {
				stmt.setLong(1, idServizioRef);
				stmt.execute();
			}
		} catch (SQLQueryObjectException | SQLException e) {
			throw new DriverConfigurazioneException("Errore nella rimozione di un record di tipo DigestServiceParam", e);
		} finally {
			this.driverConfigurazioneDB.closeConnection(conn);
		}
		
		return true;
	}
	
	
	/**
	 * Rimuove partendo dalle informazioni piu vecchie tutte le informazioni crittografiche relative ad un determinato 
	 * servizio fino ad averne al piu n
	 * @param idServizio: id del servizio relativo
	 * @param n: numero di informazioni massimo da tenere in memoria
	 * @return
	 * @throws DriverConfigurazioneException: nel caso la rimozione non vada a buon fine
	 */
	public boolean removeOldEntries(IDServizio idServizio, int n) throws DriverConfigurazioneException {
		Connection conn = null;
		String tipoDB = this.driverConfigurazioneDB.getTipoDB();
		try { 
			conn = this.driverConfigurazioneDB.getConnection("removeEntries");
			
			// ottengo l'id di riferimento del servizio
			Long idServizioRef = this.getIdServizio(conn, tipoDB, idServizio);
			if (idServizioRef == null)
				return false;
			
			// ottengo il timestamp della n-esimo record
			Timestamp lastQuery = null;
			ISQLQueryObject query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
			query.addFromTable(CostantiDB.SERVIZI_DIGEST_PARAMS);
			query.addSelectField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE);
			query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ID_SERVIZIO_REF + "= ?");
			query.addOrderBy(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE, false);
			query.setANDLogicOperator(true);
			query.setLimit(1);
			query.setOffset(n);
			try (PreparedStatement stmt = conn.prepareStatement(query.createSQLQuery())) {
				stmt.setLong(1, idServizioRef);
				
				try (ResultSet rs = stmt.executeQuery()) {
					if (rs.next()) {
						lastQuery = rs.getTimestamp(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE);
					}
				}
			}
			
			// se non esiste ci sono meno di n records
			if (lastQuery == null)
				return true;
			
			// elimino tutti i record con un timestamp precedente
			query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
			query.addDeleteTable(CostantiDB.SERVIZI_DIGEST_PARAMS);
			query.setANDLogicOperator(true);
			query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ID_SERVIZIO_REF + "= ?");
			query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE + "<= ?");
			
			try (PreparedStatement stmt = conn.prepareStatement(query.createSQLDelete())) {
				int index = 1;
				stmt.setLong(index++, idServizioRef);
				stmt.setTimestamp(index++, lastQuery);
				stmt.execute();
			}
		} catch (SQLQueryObjectException | SQLException e) {
			throw new DriverConfigurazioneException("Errore nell'aggiunta di un record di tipo DigestServiceParam", e);
		} finally {
			this.driverConfigurazioneDB.closeConnection(conn);
		}
		
		return true;
	}
	
	/**
	 * Aggiunge una nuova informazioni crittografica, questa funzione deve essere chiamata usando il lock fornito dalla classe,
	 * nel caso l'aggiunta vada a buon fine la nuova informazione aggiunta verra registrata in cache come informazione piu
	 * recente
	 * @param params: informazione crittografica da aggiungere
	 * @return
	 * @throws DriverConfigurazioneException: nel caso l'aggiunta non vada a buon fine
	 */
	public boolean addNewEntry(DigestServiceParams params)  throws DriverConfigurazioneException {
		Connection conn = null;
		String tipoDB = this.driverConfigurazioneDB.getTipoDB();
	
		try { 
			conn = this.driverConfigurazioneDB.getConnection("addEntry");
			
			// ottengo l'id di riferimento del servizio
			Long idServizioRef = this.getIdServizio(conn, tipoDB, params.getIdServizio());
			if (idServizioRef == null)
				return false;
			
			// aggiungo il nuovo elemento nel db
			ISQLQueryObject query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
			query.addInsertTable(CostantiDB.SERVIZI_DIGEST_PARAMS);
			if (params.getSerialNumber() != null)
				query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_SERIAL_NUMBER, "?");
			query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ALGORITHM, "?");
			query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE, "?");
			query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ID_SERVIZIO_REF, "?");
			query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_PERIOD, "?");
			query.addInsertField(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_SEED, "?");
			
			try (PreparedStatement stmt = conn.prepareStatement(query.createSQLInsert())) {
				int index = 1;
				if (params.getSerialNumber() != null)
					stmt.setLong(index++, params.getSerialNumber());
				stmt.setString(index++, params.getDigestAlgorithm().toString());
				stmt.setTimestamp(index++, Timestamp.from(params.getDataRegistrazione()));
				stmt.setLong(index++, idServizioRef);
				stmt.setInt(index++, params.getDurata());
				stmt.setString(index++, new String(params.getSeed()));
				stmt.execute();
			}
		} catch (SQLQueryObjectException | SQLException e) {
			throw new DriverConfigurazioneException (e);
		} finally {
			this.driverConfigurazioneDB.closeConnection(conn);
		}
		
		// aggiungo i parametri in cache (se ottengo un exception posso ignorarla in quanto non invalida il processo)
		try {
			ConfigurazionePdDReader.getCache().put(getKey(params.getIdServizio(), null), params);
		} catch (UtilsException e) {
			this.logger.warn("Errore nell'aggiunta di un valore in cache", e);
		}
		
		return true;
	}
	
	public boolean isValid(DigestServiceParams param) {
		if (param == null)
			return false;
		Instant now = Instant.now();
		Instant expiration = param
				.getDataRegistrazione()
				.plus(Duration.ofDays(param.getDurata()));
		
		return now.isBefore(expiration);
	}
	
	/**
	 * Funzione che se possible ritorna l'informazione crittografica piu recente se e solo se e' ancora valida
	 * @param idServizio
	 * @return null se l'informazione crittografica piu recente non risulta valida
	 * @throws DriverConfigurazioneException
	 */
	public DigestServiceParams getValidEntry(IDServizio idServizio) throws DriverConfigurazioneException {
		DigestServiceParams param = this.getLastEntry(idServizio);
		if (isValid(param))
			return param;
		return null;
	}
	
	/**
	 * Funzione che ritorna l'informazione crittografica piu recente
	 * @param idServizio
	 * @return
	 * @throws DriverConfigurazioneException
	 */
	public DigestServiceParams getLastEntry(IDServizio idServizio) throws DriverConfigurazioneException {
		return this.getEntry(idServizio, null);
	}
	
	/**
	 * Metodo che ritorna l'informazione crittografica relativa ad un servizio dato un determinato numero seriale
	 * @param idServizio
	 * @param serialNumber
	 * @return
	 * @throws DriverConfigurazioneException
	 */
	public DigestServiceParams getEntry(IDServizio idServizio, Long serialNumber) throws DriverConfigurazioneException {
		Connection conn = null;
		String tipoDB = this.driverConfigurazioneDB.getTipoDB();
		DigestServiceParams param  = null;
		
		try {
			// cerco di ottenere i parametri dalla cache se e solo se il numero seriale e' definito o se il record piu recente e' ancora valido
			param = (DigestServiceParams) ConfigurazionePdDReader.getRawObjectCache(getKey(idServizio, serialNumber));
			if (param != null && (serialNumber != null || isValid(param)))
				return param;
			
			conn = this.driverConfigurazioneDB.getConnection("getEntry");
			
			// ottengo l'id di riferimento del servizio
			Long idServizioRef = this.getIdServizio(conn, tipoDB, idServizio);
			if (idServizioRef == null)
				throw new DriverConfigurazioneException("idServizio riferito non presente nella tabella servizio");
			
			ISQLQueryObject query = SQLObjectFactory.createSQLQueryObject(tipoDB); 
			query.addFromTable(CostantiDB.SERVIZI_DIGEST_PARAMS);
			query.setANDLogicOperator(true);
			query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_ID_SERVIZIO_REF + "= ?");
			if (serialNumber != null)
				query.addWhereCondition(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_SERIAL_NUMBER + "= ?");
			query.addOrderBy(CostantiDB.SERVIZI_DIGEST_PARAMS_COLUMN_DATE, false);
			query.setLimit(1);
			
			try (PreparedStatement stmt = conn.prepareStatement(query.createSQLQuery())) {
				int index = 1;
				stmt.setLong(index++, idServizioRef);
				if (serialNumber != null)
					stmt.setLong(index++, serialNumber);
				stmt.execute();
				
				try (ResultSet rs = stmt.executeQuery()) {
					if (rs.next()) {
						param = this.paramsFromResultSet(idServizio, rs);
					}
				}
			}
			
		} catch (SQLQueryObjectException | SQLException e) {
			throw new DriverConfigurazioneException("Errore nella get del DigestServiceParam", e);
		} finally {
			this.driverConfigurazioneDB.closeConnection(conn);
		}
	
		// aggiorno la cache se e solo se il numero seriale e' esplicitato o il record e' valido
		try {
			if (param != null && (serialNumber != null || isValid(param)))
				ConfigurazionePdDReader.getCache().put(getKey(idServizio, serialNumber), param);
		} catch (UtilsException e) {
			this.logger.warn("Errore nell'aggiunta di un valore in cache", e);
		}
		
		return param;
	}
	
	private Semaphore semaphore;
	private SemaphoreLock lock;
	
	private Semaphore getSemaphore() throws UtilsException {
		if (this.semaphore == null) {
			InfoStatistics semaphoreStatistics = new InfoStatistics();
			
			SemaphoreConfiguration config = GestoreMessaggi.newSemaphoreConfiguration(1000, 1000);
			
			this.semaphore = new Semaphore(semaphoreStatistics, SemaphoreMapping.newInstance(DB_LOCK_ID), 
				config,
				TipiDatabase.toEnumConstant(this.driverConfigurazioneDB.getTipoDB()),
				DriverConfigurazioneDB.getCheckLogger());
		}
		
		return this.semaphore;
	}
	
	/**
	 * Ottiene un lock (globale e locale) sulla tabella delle informazioni crittografiche
	 * @param idTransazione, id della transazione corrente
	 * @throws UtilsException
	 * @throws DriverConfigurazioneException
	 */
	public void acquireLock(String idTransazione) throws UtilsException, DriverConfigurazioneException {
		Connection con = null;
		try {
			con = this.driverConfigurazioneDB.getConnection("acquireLockDigestService");
			
			while(!this.getSemaphore().newLock(con, "acquireLockDigestService")) {
				Utilities.sleep(1);
			}
			
			this.lock = THREAD_LOCK.acquire("acquireLock", idTransazione);
			
		} catch (UtilsException | DriverConfigurazioneException e) {
			this.releaseLock();
		} finally {
			this.driverConfigurazioneDB.releaseConnection(con);
		}
	}
	
	/**
	 * Rilascia il lock (globale e locale) sulla tabella delle informazioni crittografiche
	 * @throws UtilsException
	 * @throws DriverConfigurazioneException
	 */
	public void releaseLock() throws UtilsException, DriverConfigurazioneException {
		Connection con = null;
		try {
			THREAD_LOCK.release(this.lock, "acquireLock");
			con = this.driverConfigurazioneDB.getConnection("releaseLockDigestService");
			this.getSemaphore().releaseLock(con, "releaseLockDigestService");
		} finally {
			this.driverConfigurazioneDB.releaseConnection(con);
		}
	}
	
	
	
}