/** * This file is part of Waarp Project. * * Copyright 2009, Frederic Bregier, and individual contributors by the @author tags. See the * COPYRIGHT.txt in the distribution for a full listing of individual contributors. * * All Waarp Project is free software: you can redistribute it and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Waarp 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 Waarp. If not, see * <http://www.gnu.org/licenses/>. */ package org.waarp.common.database; import java.sql.Connection; import java.sql.SQLException; import java.sql.Savepoint; import java.util.ConcurrentModificationException; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import io.netty.util.Timeout; import io.netty.util.TimerTask; import io.netty.util.internal.ConcurrentSet; import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException; import org.waarp.common.database.exception.WaarpDatabaseSqlException; import org.waarp.common.database.model.DbModel; import org.waarp.common.database.model.DbModelFactory; import org.waarp.common.logging.WaarpLogger; import org.waarp.common.logging.WaarpLoggerFactory; import org.waarp.common.utility.UUID; // Notice, do not import com.mysql.jdbc.* // or you will have problems! /** * Class to handle session with the SGBD * * @author Frederic Bregier * */ public class DbSession { /** * Internal Logger */ private static final WaarpLogger logger = WaarpLoggerFactory .getLogger(DbSession.class); /** * DbAdmin referent object */ private DbAdmin admin = null; /** * The internal connection */ private Connection conn = null; /** * Is this connection Read Only */ private boolean isReadOnly = true; /** * Is this session using AutoCommit (true by default) */ private boolean autoCommit = true; /** * Internal Id */ private UUID internalId; /** * Number of threads using this connection */ private AtomicInteger nbThread = new AtomicInteger(0); /** * To be used when a local Channel is over */ private volatile boolean isDisActive = true; /** * List all DbPrepareStatement with long term usage to enable the recreation when the associated * connection is reopened */ private final Set<DbPreparedStatement> listPreparedStatement = new ConcurrentSet<DbPreparedStatement>(); private void initialize(DbModel dbModel, String server, String user, String passwd, boolean isReadOnly, boolean autoCommit) throws WaarpDatabaseNoConnectionException { if (!DbModelFactory.classLoaded.contains(dbModel.getDbType().name())) { throw new WaarpDatabaseNoConnectionException( "DbAdmin not initialzed"); } if (server == null) { setConn(null); logger.error("Cannot set a null Server"); throw new WaarpDatabaseNoConnectionException( "Cannot set a null Server"); } try { this.setAutoCommit(autoCommit); setConn(dbModel.getDbConnection(server, user, passwd)); getConn().setAutoCommit(this.isAutoCommit()); this.setReadOnly(isReadOnly); getConn().setReadOnly(this.isReadOnly()); setInternalId(new UUID()); logger.debug("Open Db Conn: " + getInternalId()); DbAdmin.addConnection(getInternalId(), this); setDisActive(false); checkConnection(); } catch (SQLException ex) { setDisActive(true); // handle any errors logger.error("Cannot create Connection"); error(ex); if (getConn() != null) { try { getConn().close(); } catch (SQLException e) { } } setConn(null); throw new WaarpDatabaseNoConnectionException( "Cannot create Connection", ex); } } /** * Create a session and connect the current object to the server using the DbAdmin object. The * database access use auto commit. * * If the initialize is not call before, call it with the default value. * * @param admin * @param isReadOnly * @throws WaarpDatabaseSqlException */ public DbSession(DbAdmin admin, boolean isReadOnly) throws WaarpDatabaseNoConnectionException { try { this.setAdmin(admin); initialize(admin.getDbModel(), admin.getServer(), admin.getUser(), admin.getPasswd(), isReadOnly, true); } catch (NullPointerException ex) { // handle any errors setDisActive(true); logger.error("Cannot create Connection:" + (admin == null), ex); if (getConn() != null) { try { getConn().close(); } catch (SQLException e) { } } setConn(null); throw new WaarpDatabaseNoConnectionException( "Cannot create Connection", ex); } } /** * Create a session and connect the current object to the server using the DbAdmin object. * * If the initialize is not call before, call it with the default value. * * @param admin * @param isReadOnly * @param autoCommit * @throws WaarpDatabaseSqlException */ public DbSession(DbAdmin admin, boolean isReadOnly, boolean autoCommit) throws WaarpDatabaseNoConnectionException { try { this.setAdmin(admin); initialize(admin.getDbModel(), admin.getServer(), admin.getUser(), admin.getPasswd(), isReadOnly, autoCommit); } catch (NullPointerException ex) { // handle any errors logger.error("Cannot create Connection:" + (admin == null), ex); setDisActive(true); if (getConn() != null) { try { getConn().close(); } catch (SQLException e) { } } setConn(null); throw new WaarpDatabaseNoConnectionException( "Cannot create Connection", ex); } } /** * Change the autocommit feature * * @param autoCommit * @throws WaarpDatabaseNoConnectionException */ public void setAutoCommit(boolean autoCommit) throws WaarpDatabaseNoConnectionException { if (getConn() != null) { this.autoCommit = autoCommit; try { getConn().setAutoCommit(autoCommit); } catch (SQLException e) { // handle any errors logger.error("Cannot create Connection"); error(e); if (getConn() != null) { try { getConn().close(); } catch (SQLException e1) { } } setConn(null); setDisActive(true); throw new WaarpDatabaseNoConnectionException( "Cannot create Connection", e); } } } /** * @return the admin */ public DbAdmin getAdmin() { return admin; } /** * @param admin * the admin to set */ protected void setAdmin(DbAdmin admin) { this.admin = admin; } /** * Print the error from SQLException * * @param ex */ public static void error(SQLException ex) { // handle any errors logger.error("SQLException: " + ex.getMessage() + " SQLState: " + ex.getSQLState() + "VendorError: " + ex.getErrorCode()); } /** * To be called when a client will start to use this DbSession (once by client) */ public void useConnection() { int val = nbThread.incrementAndGet(); synchronized (this) { if (isDisActive()) { try { initialize(getAdmin().getDbModel(), getAdmin().getServer(), getAdmin().getUser(), getAdmin().getPasswd(), isReadOnly(), isAutoCommit()); } catch (WaarpDatabaseNoConnectionException e) { logger.error("ThreadUsing: " + nbThread + " but not connected"); return; } } } logger.debug("ThreadUsing: " + val); } /** * To be called when a client will stop to use this DbSession (once by client) */ public void endUseConnection() { int val = nbThread.decrementAndGet(); logger.debug("ThreadUsing: " + val); if (val <= 0) { disconnect(); } } /** * To be called when a client will stop to use this DbSession (once by client). * This version is not blocking. */ public void enUseConnectionNoDisconnect() { int val = nbThread.decrementAndGet(); logger.debug("ThreadUsing: " + val); if (val <= 0) { DbAdmin.dbSessionTimer.newTimeout(new TryDisconnectDbSession(this), DbAdmin.WAITFORNETOP * 10, TimeUnit.MILLISECONDS); } } /** * To disconnect in asynchronous way the DbSession * * @author "Frederic Bregier" * */ private static class TryDisconnectDbSession implements TimerTask { private final DbSession dbSession; private TryDisconnectDbSession(DbSession dbSession) { this.dbSession = dbSession; } public void run(Timeout timeout) throws Exception { int val = dbSession.nbThread.get(); if (val <= 0) { dbSession.disconnect(); } logger.debug("ThreadUsing: " + val); } } @Override public int hashCode() { return this.getInternalId().hashCode(); } @Override public boolean equals(Object o) { if (o == null || !(o instanceof DbSession)) return false; return (this == o) || this.getInternalId().equals(((DbSession) o).getInternalId()); } /** * Force the close of the connection */ public void forceDisconnect() { if (this.getInternalId().equals(getAdmin().getSession().getInternalId())) { logger.debug("Closing internal db connection"); } this.nbThread.set(0); if (getConn() == null) { logger.debug("Connection already closed"); return; } try { Thread.sleep(DbAdmin.WAITFORNETOP); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } logger.debug("DbConnection still in use: " + nbThread); removeLongTermPreparedStatements(); DbAdmin.removeConnection(getInternalId()); setDisActive(true); try { logger.debug("Fore close Db Conn: " + getInternalId()); if (getConn() != null) { getConn().close(); setConn(null); } } catch (SQLException e) { logger.warn("Disconnection not OK"); error(e); } catch (ConcurrentModificationException e) { // ignore } logger.info("Current cached connection: " + getAdmin().getDbModel().currentNumberOfPooledConnections()); } /** * Close the connection * */ public void disconnect() { if (this.getInternalId().equals(getAdmin().getSession().getInternalId())) { logger.debug("Closing internal db connection: " + nbThread.get()); } if (getConn() == null || isDisActive()) { logger.debug("Connection already closed"); return; } try { Thread.sleep(DbAdmin.WAITFORNETOP); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); } logger.debug("DbConnection still in use: " + nbThread); if (nbThread.get() > 0) { logger.info("Still some clients could use this Database Session: " + nbThread); return; } synchronized (this) { removeLongTermPreparedStatements(); DbAdmin.removeConnection(getInternalId()); setDisActive(true); try { logger.debug("Close Db Conn: " + getInternalId()); if (getConn() != null) { getConn().close(); setConn(null); } } catch (SQLException e) { logger.warn("Disconnection not OK"); error(e); } catch (ConcurrentModificationException e) { // ignore } } logger.info("Current cached connection: " + getAdmin().getDbModel().currentNumberOfPooledConnections()); } /** * Check the connection to the Database and try to reopen it if possible * * @throws WaarpDatabaseNoConnectionException */ public void checkConnection() throws WaarpDatabaseNoConnectionException { try { getAdmin().getDbModel().validConnection(this); setDisActive(false); if (getAdmin() != null) getAdmin().setActive(true); } catch (WaarpDatabaseNoConnectionException e) { setDisActive(true); if (getAdmin() != null) getAdmin().setActive(false); throw e; } } /** * * @return True if the connection was successfully reconnected */ public boolean checkConnectionNoException() { try { checkConnection(); return true; } catch (WaarpDatabaseNoConnectionException e) { return false; } } /** * Add a Long Term PreparedStatement * * @param longterm */ public void addLongTermPreparedStatement(DbPreparedStatement longterm) { this.listPreparedStatement.add(longterm); } /** * Due to a reconnection, recreate all associated long term PreparedStatements * * @throws WaarpDatabaseNoConnectionException * @throws WaarpDatabaseSqlException */ public void recreateLongTermPreparedStatements() throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException { WaarpDatabaseNoConnectionException elast = null; WaarpDatabaseSqlException e2last = null; logger.info("RecreateLongTermPreparedStatements: " + listPreparedStatement.size()); for (DbPreparedStatement longterm : listPreparedStatement) { try { longterm.recreatePreparedStatement(); } catch (WaarpDatabaseNoConnectionException e) { logger.warn( "Error while recreation of Long Term PreparedStatement", e); elast = e; } catch (WaarpDatabaseSqlException e) { logger.warn( "Error while recreation of Long Term PreparedStatement", e); e2last = e; } } if (elast != null) { throw elast; } if (e2last != null) { throw e2last; } } /** * Remove all Long Term PreparedStatements (closing connection) */ public void removeLongTermPreparedStatements() { for (DbPreparedStatement longterm : listPreparedStatement) { if (longterm != null) { longterm.realClose(); } } listPreparedStatement.clear(); } /** * Remove one Long Term PreparedStatement * * @param longterm */ public void removeLongTermPreparedStatements(DbPreparedStatement longterm) { listPreparedStatement.remove(longterm); } /** * Commit everything * * @throws WaarpDatabaseSqlException * @throws WaarpDatabaseNoConnectionException */ public void commit() throws WaarpDatabaseSqlException, WaarpDatabaseNoConnectionException { if (getConn() == null) { logger.warn("Cannot commit since connection is null"); throw new WaarpDatabaseNoConnectionException( "Cannot commit since connection is null"); } if (this.isAutoCommit()) { return; } if (isDisActive()) { checkConnection(); } try { getConn().commit(); } catch (SQLException e) { logger.error("Cannot Commit"); error(e); throw new WaarpDatabaseSqlException("Cannot commit", e); } } /** * Rollback from the savepoint or the last set if null * * @param savepoint * @throws WaarpDatabaseNoConnectionException * @throws WaarpDatabaseSqlException */ public void rollback(Savepoint savepoint) throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException { if (getConn() == null) { logger.warn("Cannot rollback since connection is null"); throw new WaarpDatabaseNoConnectionException( "Cannot rollback since connection is null"); } if (isDisActive()) { checkConnection(); } try { if (savepoint == null) { getConn().rollback(); } else { getConn().rollback(savepoint); } } catch (SQLException e) { logger.error("Cannot rollback"); error(e); throw new WaarpDatabaseSqlException("Cannot rollback", e); } } /** * Make a savepoint * * @return the new savepoint * @throws WaarpDatabaseNoConnectionException * @throws WaarpDatabaseSqlException */ public Savepoint savepoint() throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException { if (getConn() == null) { logger.warn("Cannot savepoint since connection is null"); throw new WaarpDatabaseNoConnectionException( "Cannot savepoint since connection is null"); } if (isDisActive()) { checkConnection(); } try { return getConn().setSavepoint(); } catch (SQLException e) { logger.error("Cannot savepoint"); error(e); throw new WaarpDatabaseSqlException("Cannot savepoint", e); } } /** * Release the savepoint * * @param savepoint * @throws WaarpDatabaseNoConnectionException * @throws WaarpDatabaseSqlException */ public void releaseSavepoint(Savepoint savepoint) throws WaarpDatabaseNoConnectionException, WaarpDatabaseSqlException { if (getConn() == null) { logger.warn("Cannot release savepoint since connection is null"); throw new WaarpDatabaseNoConnectionException( "Cannot release savepoint since connection is null"); } if (isDisActive()) { checkConnection(); } try { getConn().releaseSavepoint(savepoint); } catch (SQLException e) { logger.error("Cannot release savepoint"); error(e); throw new WaarpDatabaseSqlException("Cannot release savepoint", e); } } /** * @return the isReadOnly */ public boolean isReadOnly() { return isReadOnly; } /** * @param isReadOnly the isReadOnly to set */ public void setReadOnly(boolean isReadOnly) { this.isReadOnly = isReadOnly; } /** * @return the autoCommit */ public boolean isAutoCommit() { return autoCommit; } /** * @return the conn */ public Connection getConn() { return conn; } /** * @param conn the conn to set */ public void setConn(Connection conn) { this.conn = conn; } /** * @return the internalId */ public UUID getInternalId() { return internalId; } /** * @param internalId the internalId to set */ private void setInternalId(UUID internalId) { this.internalId = internalId; } /** * @return the isDisActive */ public boolean isDisActive() { return isDisActive; } /** * @param isDisActive the isDisActive to set */ public void setDisActive(boolean isDisActive) { this.isDisActive = isDisActive; } }