DbModelAbstract.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.model;

import org.postgresql.util.PSQLException;
import org.waarp.common.database.DbAdmin;
import org.waarp.common.database.DbConstant;
import org.waarp.common.database.DbSession;
import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
import org.waarp.common.database.exception.WaarpDatabaseSqlException;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ConcurrentModificationException;

/**
 * This Abstract class regroups common methods for all implementation classes.
 */
public abstract class DbModelAbstract implements DbModel {
  /**
   * Max size in Binary mode, store in BASE64 mode (so x2)
   */
  public static final int MAX_BINARY = 255;
  /**
   * Max size in LONGVARCHAR (except ORACLE = 4000)
   */
  public static final int MAX_LONGVARCHAR = 12 * 1024;
  /**
   * Max Key size in VARCHAR
   */
  public static final int MAX_KEY_VARCHAR = 255;
  /**
   * Max VARCHAR size (except ORACLE = 4000)
   */
  public static final int MAX_VARCHAR = 8096;
  protected static DbTypeResolver dbTypeResolver;

  public abstract static class DbTypeResolver {
    public abstract String getType(int sqlType);

    public String getCreateTable() {
      return "CREATE TABLE ";
    }

    public String getPrimaryKey() {
      return " PRIMARY KEY ";
    }

    public String getNotNull() {
      return " NOT NULL ";
    }

    public String getCreateIndex() {
      return "CREATE INDEX ";
    }

    public abstract DbType getDbType();
  }

  public DbTypeResolver getDbTypeResolver() {
    return dbTypeResolver;
  }

  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(DbModelAbstract.class);
  private static final String CANNOT_CONNECT_TO_DATABASE =
      "Cannot connect to database";

  /**
   * Recreate the disActive session
   *
   * @param dbSession
   *
   * @throws WaarpDatabaseNoConnectionException
   */
  private void recreateSession(final DbSession dbSession)
      throws WaarpDatabaseNoConnectionException {
    DbAdmin admin = dbSession.getAdmin();
    if (admin == null) {
      if (dbSession.isAutoCommit()) {
        admin = DbConstant.admin;
      } else {
        admin = DbConstant.noCommitAdmin;
      }
    }
    final DbSession newdbSession;
    newdbSession = new DbSession(admin, dbSession.isReadOnly());
    try {
      if (dbSession.getConn() != null) {
        dbSession.getConn().close();
      }
    } catch (final SQLException ignored) {
      // nothing
    } catch (final ConcurrentModificationException ignored) {
      // nothing
    }
    dbSession.setConn(newdbSession.getConn());
    DbAdmin.addConnection(dbSession.getInternalId(), dbSession);
    DbAdmin.removeConnection(newdbSession.getInternalId());
    logger.warn("Database Connection lost: database connection reopened");
  }

  /**
   * Internal use for closing connection while validating it
   *
   * @param dbSession
   */
  protected final void closeInternalConnection(final DbSession dbSession) {
    try {
      if (dbSession.getConn() != null) {
        dbSession.getConn().close();
      }
    } catch (final SQLException ignored) {
      // nothing
    } catch (final ConcurrentModificationException ignored) {
      // nothing
    }
    dbSession.setDisActive(true);
    DbAdmin.removeConnection(dbSession.getInternalId());
  }

  @Override
  public final void validConnection(final DbSession dbSession)
      throws WaarpDatabaseNoConnectionException {
    // try to limit the number of check!
    synchronized (this) {
      if (dbSession.getConn() == null) {
        throw new WaarpDatabaseNoConnectionException(
            CANNOT_CONNECT_TO_DATABASE);
      }
      try {
        if (!dbSession.getConn().isClosed() &&
            !dbSession.getConn().isValid(DbConstant.VALIDTESTDURATION)) {
          // Give a try by closing the current connection
          throw new SQLException(CANNOT_CONNECT_TO_DATABASE);
        }
        dbSession.setDisActive(false);
      } catch (final SQLException e2) {
        dbSession.setDisActive(true);
        // Might be unsupported so switch to SELECT 1 way
        if (e2 instanceof PSQLException) {
          validConnectionSelect(dbSession);
          return;
        }
        if (subValidationConnection(dbSession, e2)) {
          return;
        }
        closeInternalConnection(dbSession);
        throw new WaarpDatabaseNoConnectionException(CANNOT_CONNECT_TO_DATABASE,
                                                     e2);
      }
    }
  }

  private boolean subValidationConnection(final DbSession dbSession,
                                          final SQLException e2)
      throws WaarpDatabaseNoConnectionException {
    try {
      try {
        recreateSession(dbSession);
      } catch (final WaarpDatabaseNoConnectionException e) {
        closeInternalConnection(dbSession);
        throw e;
      }
      try {
        if (!dbSession.getConn().isValid(DbConstant.VALIDTESTDURATION)) {
          // Not ignored
          closeInternalConnection(dbSession);
          throw new WaarpDatabaseNoConnectionException(
              CANNOT_CONNECT_TO_DATABASE, e2);
        }
      } catch (final SQLException e) {
        closeInternalConnection(dbSession);
        throw new WaarpDatabaseNoConnectionException(CANNOT_CONNECT_TO_DATABASE,
                                                     e);
      }
      dbSession.setDisActive(false);
      dbSession.recreateLongTermPreparedStatements();
      return true;
    } catch (final WaarpDatabaseSqlException e1) {
      // ignore and will send a No Connection error
    }
    return false;
  }

  protected final void validConnectionSelect(final DbSession dbSession)
      throws WaarpDatabaseNoConnectionException {
    // try to limit the number of check!
    synchronized (this) {
      Statement stmt = null;
      try {
        stmt = dbSession.getConn().createStatement();//NOSONAR
        if (stmt.execute(validConnectionString())) {
          ResultSet set = null;
          try {
            set = stmt.getResultSet();
            if (!set.next()) {
              closingStatement(stmt);
              // Give a try by closing the current connection
              throw new SQLException(CANNOT_CONNECT_TO_DATABASE);
            }
          } finally {
            if (set != null) {
              set.close();
            }
          }
        }
        dbSession.setDisActive(false);
      } catch (final SQLException e2) {
        dbSession.setDisActive(true);
        stmt = subValidConnectionSelect(dbSession, stmt);
        if (stmt == null) {
          return;
        }
        closeInternalConnection(dbSession);
        throw new WaarpDatabaseNoConnectionException(CANNOT_CONNECT_TO_DATABASE,
                                                     e2);
      } finally {
        closingStatement(stmt);
      }
    }
  }

  private void closingStatement(final Statement stmt) {
    try {
      if (stmt != null) {
        stmt.close();
      }
    } catch (final SQLException e) {
      // ignore
    }
  }

  private Statement subValidConnectionSelect(final DbSession dbSession,
                                             Statement stmt)
      throws WaarpDatabaseNoConnectionException {
    try {
      try {
        recreateSession(dbSession);
      } catch (final WaarpDatabaseNoConnectionException e) {
        closeInternalConnection(dbSession);
        throw e;
      }
      closingStatement(stmt);
      try {
        stmt = dbSession.getConn().createStatement();//NOSONAR
      } catch (final SQLException e) {
        // Not ignored
        closeInternalConnection(dbSession);
        throw new WaarpDatabaseNoConnectionException(CANNOT_CONNECT_TO_DATABASE,
                                                     e);
      }
      try {
        if (stmt.execute(validConnectionString())) {
          ResultSet set = null;
          try {
            set = stmt.getResultSet();
            if (!set.next()) {
              closingStatement(stmt);
              closeInternalConnection(dbSession);
              throw new WaarpDatabaseNoConnectionException(
                  CANNOT_CONNECT_TO_DATABASE);
            }
          } finally {
            if (set != null) {
              set.close();
            }
          }
        }
      } catch (final SQLException e) {
        closingStatement(stmt);
        closeInternalConnection(dbSession);
        throw new WaarpDatabaseNoConnectionException(CANNOT_CONNECT_TO_DATABASE,
                                                     e);
      }
      dbSession.setDisActive(false);
      dbSession.recreateLongTermPreparedStatements();
      closingStatement(stmt);
      return null;
    } catch (final WaarpDatabaseSqlException e1) {
      // ignore and will send a No Connection error
    }
    return stmt;
  }

  /**
   * @return the associated String to validate the connection (as "select 1
   *     from
   *     dual")
   */
  protected abstract String validConnectionString();

  @Override
  public Connection getDbConnection(final String server, final String user,
                                    final String passwd) throws SQLException {
    // Default implementation
    return DriverManager.getConnection(server, user, passwd);
  }

  @Override
  public void releaseResources() {
  }

  @Override
  public int currentNumberOfPooledConnections() {
    return DbAdmin.getNbConnection();
  }

}