Package com.google.enterprise.connector.util.database

Source Code of com.google.enterprise.connector.util.database.JdbcDatabase

// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.enterprise.connector.util.database;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.enterprise.connector.spi.DatabaseResourceBundle;
import com.google.enterprise.connector.spi.SpiConstants.DatabaseType;
import com.google.enterprise.connector.util.BasicChecksumGenerator;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sql.DataSource;

/**
* Basic database connectivity, table creation and connection pooling.
* <p/>
* The JdbcDatabase wraps a {@link javax.sql.DataSource} instance,
* adding additional features specifically beneficial to Connectors
* and the ConnectorManager, such as:
* <ul><li>Information regarding the underlying database implementation,
* including the vendor name, version information, and description.</li>
* <li>Maintaining a ConnectionPool for the DataSource.</li>
* <li>Manufacturing legal database table names base upon a Connector name.</li>
* <li>Creating database tables based upon a supplied DDL, or verifying
* that such a table exists.</li>
* <li>Methods useful for forming DatabaseResourceBundle filenames for
* the specific vendor implementation.</li>
* </ul>
*
* @since 2.8
*/
public class JdbcDatabase {
  private static final Logger LOGGER =
      Logger.getLogger(JdbcDatabase.class.getName());

  private final DataSource dataSource;
  private final DatabaseConnectionPool connectionPool;

  private DatabaseType databaseType;
  private String productName;
  private String description;
  private String resourceBundleExtension;

  public JdbcDatabase(DataSource dataSource) {
    this.dataSource = dataSource;
    this.connectionPool = new DatabaseConnectionPool(dataSource);
    LOGGER.config("Using JDBC DataSource: " + dataSource.toString());
    getDatabaseInfo();
  }

  @Override
  protected void finalize() throws Throwable {
    shutdown();
  }

  public synchronized void shutdown() {
    connectionPool.closeConnections();
  }

  /**
   * Returns {@code true} if the configured {@code JdbcDatabase} is unavailable.
   *
   * @return {@code true} if this {@code JdbcDatabase} is disabled, {@code false}
   * otherwise.
   */
  public boolean isDisabled() {
    // If I can successfully establish a Connection to the database, assume
    // the DataSource is functional.  Otherwise, consider it disabled.
    try {
      dataSource.getConnection().close();
      return false;
    } catch (SQLException e) {
      return true;
    }
  }

  /**
   * Return the underlying {@link DataSource} for the database instance.
   *
   *
   * @return the underlying {@link DataSource} for the database instance.
   */
  public DataSource getDataSource() {
    return dataSource;
  }

  /**
   * Return the {@link DatabaseConnectionPool} used to maintain a collection
   * of opened {@link Connection Connections} to the {@link DataSource}.
   *
   * @return the {@link DatabaseConnectionPool} for this database
   */
  public DatabaseConnectionPool getConnectionPool() {
    return connectionPool;
  }

  /**
   * Return the {@link DatabaseType} for this database.
   *
   * @return the {@link DatabaseType} for this database
   */
  public DatabaseType getDatabaseType() {
    return databaseType;
  }

  /**
   * Return a sanitzed version of the vendor product name.
   * This sanitized string is used to create the resourceBundleExtension
   * and will generally match one of the supported DatabaseTypes.
   *
   * @return the sanitized product name
   */
  public String getProductName() {
    return productName;
  }

  /**
   * Return a description string for this database.
   * This may be a vendor-supplied string or simply the unsanitized
   * product name and version string.
   *
   * @return the {@code description} string
   */
  public String getDescription() {
    return description;
  }

  @Override
  public String toString() {
    return description;
  }

  /**
   * Returns the {@link DatabaseResourceBundle} extension that may be added
   * to a {@code ResourceBundle baseName} to build the name of the resource
   * that is specific to the database implementation identified by this
   * {@code JdbcDatabase} instance.
   * <p/>
   * This is will be constructed from the database {@code productName},
   * {@code majorVersion}, and {@code minorVersion} separated by underbars
   * ({@code '_'}), for instance MySQL v5.6 will return "{@code _mysql_5_6}".
   *
   * @return the resource bundle extension
   */
  public String getResourceBundleExtension() {
    return resourceBundleExtension;
  }

  /**
   * If the supplied string is not {@code null} or empty, prepend an underscore,
   * else return the empty string.
   */
  private String bundleNameFragment(String string) {
    return Strings.isNullOrEmpty(string) ? "" : "_" + string;
  }

  /**
   * Sanitizes the supplied string for use as a portion of a
   * ResourceBundleExtension.  Specifically, a sanitized string
   * should consist of nothing other than lowercase alphabetics
   * [a-z], numerics [0-9], underscore '_', and hyphen '-'.
   * <p/>
   * The following actions are taken sanitize the input string:
   * <ul><li>Alphabetics are converted to lowercase.</li>
   * <li>Leading and trailing invalid characters are removed.</li>
   * <li>The remaining invalid characters are converted to hyphens.</li>
   * </ul>
   * <p/>
   * For example, the string "Hello World!" would sanitize to "hello-world".
   *
   * @param string the {@code String} to be sanitized.
   * @return sanitized version of the supplied {@code string}
   */
  private String sanitize(String string) {
    if (string != null) {
      return string.toLowerCase().replaceAll("[^a-z0-9_]+", " ").trim()
             .replace(' ', '-');
    }
    return string;
  }

  /**
   * Extract vendor-specific infromation about this database from
   * the DataBaseMetaData.  This will be used to identify the DatabaseType
   * and build the DatabaseResourceBundle extenstion for the database.
   */
  private void getDatabaseInfo() {
    // TODO: Support Manual Configuration of this information,
    // which would override this detection.

    // If the database is unavailable, don't try to fetch its metadata.
    if (isDisabled()) {
      databaseType = DatabaseType.OTHER;
      productName = description = "Disabled Database";
      resourceBundleExtension = "";
      return;
    }

    try {
      Connection connection = connectionPool.getConnection();
      try {
        DatabaseMetaData metaData = connection.getMetaData();
        productName = metaData.getDatabaseProductName();
        String productVersion = metaData.getDatabaseProductVersion();
        int majorVersion = metaData.getDatabaseMajorVersion();
        int minorVersion = metaData.getDatabaseMinorVersion();
        LOGGER.config("JDBC Database ProductName: " + productName);
        LOGGER.config("JDBC Database ProductVersion: " + productVersion);
        LOGGER.config("JDBC Database MajorVersion: " + majorVersion);
        LOGGER.config("JDBC Database MinorVersion: " + minorVersion);

        description = productName + " " + productVersion;
        productName = sanitize(productName);
        if (productName.equals(DatabaseType.ORACLE.toString())) {
          // Oracle already has a really long description string.
          description = productVersion.replace("\n", " ");
        } else if (productName.equals("microsoft-sql-server")) {
          // If it looks like "Microsoft SQL Server", shorten name.
          productName = DatabaseType.SQLSERVER.toString();
        } else if (productName.startsWith("db2")) {
          // IBM embellishes the DB2 productName with extra platform info.
          productName = "db2";
        } else if (productName.indexOf("derby") >= 0) {
          productName = "derby";
        } else if (productName.indexOf("firebird") >= 0) {
          productName = "firebird";
        } else if (productName.indexOf("informix") >= 0) {
          // Like anyone still uses this.
          productName = "informix";
        } else if (productName.indexOf("sybase") >= 0) {
          // Both Microsoft and Sybase products are called "SQL Server".
          productName = "sybase";
        }
        // Most of the others have simple, one-word product names.

        databaseType = DatabaseType.findDatabaseType(productName);

        // Determine the DatabaseResourceBundle extension for this DB.
        resourceBundleExtension = bundleNameFragment(productName)
            + bundleNameFragment(Integer.toString(majorVersion))
            + bundleNameFragment(Integer.toString(minorVersion));

        return;
      } finally {
        connectionPool.releaseConnection(connection);
      }
    } catch (SQLException e) {
      LOGGER.log(Level.SEVERE, "Failed to retrieve DatabaseMetaData", e);
    }

    // Fallback in case we can't get anything from DatabaseMetaData.
    databaseType = DatabaseType.OTHER;
    productName = description = "Unknown Database";
    resourceBundleExtension = "";
  }

  /**
   * Returns the maximum table name length for this database vendor.
   */
  public int getMaxTableNameLength() {
    int maxTableNameLength;
    try {
      Connection connection = connectionPool.getConnection();
      try {
        DatabaseMetaData metaData = connection.getMetaData();
        maxTableNameLength = metaData.getMaxTableNameLength();
        if (maxTableNameLength == 0) {
          maxTableNameLength = 255;
        }
      } finally {
        connectionPool.releaseConnection(connection);
      }
    } catch (SQLException e) {
      LOGGER.warning("Failed to fetch database maximum table name length.");
      maxTableNameLength = 30// Assume the worst. Oracle is 30 chars.
    }
    return maxTableNameLength;
  }

  /**
   * Constructs a database table name based up the configured table name prefix
   * and the Connector name.  All attempts are made to make this a straight
   * concatenation.  However if the connector name is too long or contains
   * invalid SQL identifier characters, then it is constructed from an MD5 hash
   * of the requested name.
   *
   * @param prefix the generated table name will begin with this prefix
   * @param connectorName the connector name
   */
  public String makeTableName(String prefix, String connectorName) {
    return makeTableName(prefix, connectorName, getMaxTableNameLength());
  }

  /**
   * Constructs a database table name based up the configured table name prefix
   * and the Connector name.  All attempts are made to make this a straight
   * concatenation.  However if the connector name is too long or contains
   * invalid SQL identifier characters, then we hash it.
   *
   * @param prefix the generated table name will begin with this prefix
   * @param connectorName the connector name
   * @param maxLength the maximum length of the generated table name
   */
  @VisibleForTesting
  static String makeTableName(String prefix, String connectorName, int maxLength) {
    prefix = Strings.nullToEmpty(prefix);
    String suffix;
    if ((connectorName.matches("[a-z0-9]+[a-z0-9_]*")) &&
        ((connectorName.length() + prefix.length()) <= maxLength)) {
      suffix = connectorName;
    } else {
      BasicChecksumGenerator sumGen = new BasicChecksumGenerator("MD5");
      suffix = sumGen.getChecksum(connectorName);
      if (prefix.length() + suffix.length() > maxLength) {
        suffix = suffix.substring(0, maxLength - prefix.length());
      }
    }
    // TODO: Match case of vendor identifiers?
    return (prefix + suffix).toLowerCase();
  }

  /**
   * Verify that a table named {@code tableName} exists in the database.
   * If not, create it, using the supplied DDL statements.
   *
   * @param tableName the name of the table to find in the database.
   * @param createTableDdl DDL statements that may be used to create the
   *        table if it does not exist.  If {@code null}, no attempt will
   *        be made to create the table.
   *
   * @return {@code true} if the table exists or was successfully created,
   *         {@code false} if the table does not exist.
   */
  public boolean verifyTableExists(String tableName, String[] createTableDdl) {
    try {
      // Try to create table.
      return verifyTableAndThrow(tableName, createTableDdl);
    } catch (SQLException e1) {
      // If that fails, we may have multiple clients trying to create the table
      // at the same time; wait a bit, then try again.
      try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
      try {
        return verifyTableAndThrow(tableName, createTableDdl);
      } catch (SQLException e2) {
        LOGGER.log(Level.SEVERE, "Failed to create table " + tableName, e2);
        return false;
      }
    }
  }

  /**
   * Verify that a table named {@code tableName} exists in the database.
   * If not, create it, using the supplied DDL statements.
   *
   * @param tableName the name of the table to find in the database.
   * @param createTableDdl DDL statements that may be used to create the
   *        table if it does not exist.  If {@code null}, no attempt will
   *        be made to create the table.
   *
   * @return {@code true} if the table exists or was successfully created,
   *         {@code false} if the table does not exist.
   * @throws SQLException if table existence could not be determined or if
   *         table creation fails.
   */
  private boolean verifyTableAndThrow(String tableName, String[] createTableDdl)
      throws SQLException {
    boolean originalAutoCommit = true;
    Connection connection = connectionPool.getConnection();
    try {
      originalAutoCommit = connection.getAutoCommit();
      connection.setAutoCommit(false);

      // Check to see if the table already exists.
      DatabaseMetaData metaData = connection.getMetaData();

      // Oracle doesn't do case-insensitive table name searches.
      String tablePattern;
      if (metaData.storesUpperCaseIdentifiers()) {
        tablePattern = tableName.toUpperCase();
      } else if (metaData.storesLowerCaseIdentifiers()) {
        tablePattern = tableName.toLowerCase();
      } else {
        tablePattern = tableName;
      }
      // Now quote '%' and '-', a special characters in search patterns.
      tablePattern =
          tablePattern.replace("%", metaData.getSearchStringEscape() + "%");
      tablePattern =
          tablePattern.replace("_", metaData.getSearchStringEscape() + "_");
      ResultSet tables = metaData.getTables(null, null, tablePattern, null);
      try {
        while (tables.next()) {
          if (tableName.equalsIgnoreCase(tables.getString("TABLE_NAME"))) {
            LOGGER.config("Found table: " + tableName);
            return true;
          }
        }
      } finally {
        tables.close();
      }

      // Our table was not found.
      if (createTableDdl == null) {
        return false;
      }

      // Create the table using the supplied Create Table DDL.
      Statement stmt = connection.createStatement();
      try {
        for (String ddlStatement : createTableDdl) {
          LOGGER.config("Creating table " + tableName + ": " + ddlStatement);
          stmt.executeUpdate(ddlStatement);
        }
        connection.commit();
      } finally {
        stmt.close();
      }
      return true;
    } catch (SQLException e) {
      try {
        connection.rollback();
      } catch (SQLException ignored) {
      }
      throw e;
    } finally {
      try {
        connection.setAutoCommit(originalAutoCommit);
      } catch (SQLException ignored) {
      }
      connectionPool.releaseConnection(connection);
    }
  }
}
TOP

Related Classes of com.google.enterprise.connector.util.database.JdbcDatabase

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.