DbAdmin.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, 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 io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
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.database.model.DbType;
import org.waarp.common.database.model.EmptyDbModel;
import org.waarp.common.guid.GUID;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpThreadFactory;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ConcurrentModificationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Class for access to Database
 *
 * Partially deprecated (only in Waarp R66, except Rest, Http and Monitoring
 * and special cases using PreparedStatements but not other components)
 */
public class DbAdmin {
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(DbAdmin.class);

  public static final int RETRYNB = 3;

  public static final long WAITFORNETOP = 100;

  /**
   * Database type
   */
  protected final DbType typeDriver;

  /**
   * DbModel
   */
  private final DbModel dbModel;

  /**
   * DB Server
   */
  private final String server;

  /**
   * DB User
   */
  private final String user;

  /**
   * DB Password
   */
  private final String passwd;

  /**
   * Is this DB Admin Read Only
   */
  private boolean isReadOnly;

  /**
   * session is the Session object for all type of requests
   */
  private DbSession session;
  /**
   * Number of HttpSession
   */
  private static int nbHttpSession;

  protected static final Timer dbSessionTimer =
      new HashedWheelTimer(new WaarpThreadFactory("TimerClose"), 50,
                           TimeUnit.MILLISECONDS, 1024);

  /**
   * @return the session
   */
  public DbSession getSession() {
    return session;
  }

  /**
   * @param session the session to set
   */
  public final void setSession(final DbSession session) {
    this.session = session;
  }

  /**
   * @return True if the connection is ReadOnly
   */
  public final boolean isReadOnly() {
    return isReadOnly;
  }

  /**
   * Validate connection
   *
   * @throws WaarpDatabaseNoConnectionException
   */
  public final void validConnection()
      throws WaarpDatabaseNoConnectionException {
    try {
      dbModel.validConnection(getSession());
    } catch (final WaarpDatabaseNoConnectionException e) {
      getSession().setDisActive(true);
      throw e;
    }
    getSession().setDisActive(false);
  }

  /**
   * Use a default server for basic connection. Later on, specific connection
   * to
   * database for the scheme that
   * provides access to the table R66DbIndex for one specific Legacy could be
   * done.
   * <p>
   * A this time, only one driver is possible! If a new driver is needed,
   * then
   * we need to create a new DbSession
   * object. Be aware that DbSession.initialize should be call only once for
   * each driver, whatever the number of
   * DbSession objects that could be created (=> need a hashtable for
   * specific
   * driver when created). Also, don't
   * know if two drivers at the same time (two different DbSession) is
   * allowed
   * by JDBC.
   *
   * @param model
   * @param server
   * @param user
   * @param passwd
   *
   * @throws WaarpDatabaseNoConnectionException
   */
  public DbAdmin(final DbModel model, final String server, final String user,
                 final String passwd)
      throws WaarpDatabaseNoConnectionException {
    this.server = server;
    this.user = user;
    this.passwd = passwd;
    dbModel = model;
    typeDriver = model.getDbType();
    if (typeDriver == null) {
      logger.error("Cannot find TypeDriver");
      throw new WaarpDatabaseNoConnectionException(
          "Cannot find database drive");
    }
    setSession(new DbSession(this, false));
    getSession().setAdmin(this);
    isReadOnly = false;
    validConnection();
    getSession().useConnection(); // default since this is the top connection
  }

  /**
   * Use a default server for basic connection. Later on, specific connection
   * to
   * database for the scheme that
   * provides access to the table R66DbIndex for one specific Legacy could be
   * done.
   * <p>
   * A this time, only one driver is possible! If a new driver is needed,
   * then
   * we need to create a new DbSession
   * object. Be aware that DbSession.initialize should be call only once for
   * each driver, whatever the number of
   * DbSession objects that could be created (=> need a hashtable for
   * specific
   * driver when created). Also, don't
   * know if two drivers at the same time (two different DbSession) is
   * allowed
   * by JDBC.
   *
   * @param model
   * @param server
   * @param user
   * @param passwd
   * @param write
   *
   * @throws WaarpDatabaseSqlException
   * @throws WaarpDatabaseNoConnectionException
   */
  public DbAdmin(final DbModel model, final String server, final String user,
                 final String passwd, final boolean write)
      throws WaarpDatabaseNoConnectionException {
    this.server = server;
    this.user = user;
    this.passwd = passwd;
    dbModel = model;
    typeDriver = model.getDbType();
    if (typeDriver == null) {
      logger.error("Cannot find TypeDriver");
      throw new WaarpDatabaseNoConnectionException(
          "Cannot find database driver");
    }
    if (write) {
      for (int i = 0; i < RETRYNB; i++) {
        try {
          setSession(new DbSession(this, false));
        } catch (final WaarpDatabaseNoConnectionException e) {
          logger.warn("Attempt of connection in error: " + i + " : {}",
                      e.getMessage());
          continue;
        }
        isReadOnly = false;
        getSession().setAdmin(this);
        validConnection();
        getSession().useConnection(); // default since this is the top
        // connection
        return;
      }
    } else {
      for (int i = 0; i < RETRYNB; i++) {
        try {
          setSession(new DbSession(this, true));
        } catch (final WaarpDatabaseNoConnectionException e) {
          logger.warn("Attempt of connection in error: " + i + " : {}",
                      e.getMessage());
          continue;
        }
        isReadOnly = true;
        getSession().setAdmin(this);
        validConnection();
        getSession().useConnection(); // default since this is the top
        // connection
        return;
      }
    }
    setSession(null);
    logger.error("Cannot connect to Database!");
    throw new WaarpDatabaseNoConnectionException("Cannot connect to database");
  }

  /**
   * Empty constructor for no Database support (very thin client)
   */
  public DbAdmin() {
    // not true but to enable pseudo database functions
    typeDriver = DbType.none;
    DbModelFactory.classLoaded.add(DbType.none.name());
    dbModel = new EmptyDbModel();
    server = null;
    user = null;
    passwd = null;
  }

  /**
   * Close the underlying session. Can be call even for connection given from
   * the constructor
   * DbAdmin(Connection, boolean).
   */
  public final void close() {
    if (getSession() != null) {
      getSession().endUseConnection(); // default since this is the top
      // connection
      getSession().forceDisconnect();
      setSession(null);
    }
  }

  /**
   * Commit on connection (since in autocommit, should not be used)
   *
   * @throws WaarpDatabaseNoConnectionException
   * @throws WaarpDatabaseSqlException
   */
  public final void commit()
      throws WaarpDatabaseSqlException, WaarpDatabaseNoConnectionException {
    if (getSession() != null) {
      getSession().commit();
    }
  }

  /**
   * @return the server
   */
  public final String getServer() {
    return server;
  }

  /**
   * @return the user
   */
  public final String getUser() {
    return user;
  }

  /**
   * @return the passwd
   */
  public final String getPasswd() {
    return passwd;
  }

  /**
   * @return the associated dbModel
   */
  public final DbModel getDbModel() {
    return dbModel;
  }

  /**
   * @return the typeDriver
   */
  public final DbType getTypeDriver() {
    return typeDriver;
  }

  @Override
  public String toString() {
    return "Admin: " + typeDriver.name() + ':' + server + ':' + user + ':' +
           (passwd == null? 0 : passwd.length());
  }

  /**
   * List all Connection to enable the close call on them
   */
  private static final ConcurrentHashMap<GUID, DbSession> listConnection =
      new ConcurrentHashMap<org.waarp.common.guid.GUID, DbSession>();

  /**
   * Increment nb of Http Connection
   */
  public static void incHttpSession() {
    nbHttpSession++;
  }

  /**
   * Decrement nb of Http Connection
   */
  public static void decHttpSession() {
    nbHttpSession--;
  }

  /**
   * @return the nb of Http Connection
   */
  public static int getHttpSession() {
    return nbHttpSession;
  }

  /**
   * Add a Connection into the list
   *
   * @param id
   * @param session
   */
  public static void addConnection(final GUID id, final DbSession session) {
    listConnection.put(id, session);
  }

  /**
   * Remove a Connection from the list
   *
   * @param id Id of the connection
   */
  public static void removeConnection(final GUID id) {
    listConnection.remove(id);
  }

  /**
   * @return the number of connection (so number of network channels)
   */
  public static int getNbConnection() {
    return listConnection.size() - 1;
  }

  /**
   * Close all database connections
   */
  public static void closeAllConnection() {
    logger.debug("DEBUG Close All Connections");
    for (final DbSession session : listConnection.values()) {
      logger.debug("Close (all) Db Conn: {}", session.getInternalId());
      try {
        final Connection connection = session.getConn();
        if (connection != null) {
          connection.close();
        }
      } catch (final SQLException ignored) {
        // nothing
      } catch (final ConcurrentModificationException ignored) {
        // nothing
      }
    }
    listConnection.clear();
    for (final DbModel dbModel : DbModelFactory.dbModels) {
      if (dbModel != null) {
        dbModel.releaseResources();
      }
    }
    dbSessionTimer.stop();
  }

  /**
   * Check all database connections and try to reopen them if disActive
   */
  public static void checkAllConnections() {
    for (final DbSession session : listConnection.values()) {
      try {
        session.checkConnection();
      } catch (final WaarpDatabaseNoConnectionException e) {
        logger.error("Database Connection cannot be reinitialized");
      }
    }
  }

  /**
   * @return True if this driver allows Thread Shared Connexion (concurrency
   *     usage)
   */
  public final boolean isCompatibleWithThreadSharedConnexion() {
    return typeDriver != DbType.MariaDB && typeDriver != DbType.MySQL &&
           typeDriver != DbType.Oracle && typeDriver != DbType.none;
  }
}