TrackingIndex.java

package gov.usgs.earthquake.aws;

import java.io.ByteArrayInputStream;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.logging.Logger;

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;

import gov.usgs.earthquake.util.JDBCConnection;
import gov.usgs.util.Config;

/**
 * Tracking index stores component state in a database.
 *
 * Only SQLITE or local development should rely on createSchema.
 *
 * Mysql Schema Example:<br>
 * <pre>
 * CREATE TABLE IF NOT EXISTS tracking_index
 * (id INTEGER PRIMARY KEY AUTO_INCREMENT
 * , created VARCHAR(255)
 * , name VARCHAR(255)
 * , data LONGTEXT
 * , UNIQUE KEY name_index (name)
 * ) ENGINE=innodb CHARSET=utf8;
 * </pre>
 */
public class TrackingIndex extends JDBCConnection {

  private static final Logger LOGGER = Logger.getLogger(
      TrackingIndex.class.getName());

  /** Variable for the default driver */
  public static final String DEFAULT_DRIVER = "org.sqlite.JDBC";
  /** Variable for the default table */
  public static final String DEFAULT_TABLE = "tracking";
  /** Variable for the default URL */
  public static final String DEFAULT_URL = "jdbc:sqlite:json_tracking_index.db";

  /** Database table name. */
  private String table;

  /**
   * Construct a TrackingIndex using defaults.
   */
  public TrackingIndex() {
    this(DEFAULT_DRIVER, DEFAULT_URL);
  }

  /**
   * Construct a TrackingIndex with the default table.
   * @param driver Driver to use
   * @param url URL to use
   */
  public TrackingIndex(final String driver, final String url) {
    this(driver, url, DEFAULT_TABLE);
  }

  /**
   * Construct a TrackingIndex with custom driver, url, and table.
   * @param driver Driver to use
   * @param url URL to use
   * @param table Table to use
   */
  public TrackingIndex(
      final String driver, final String url, final String table) {
    super(driver, url);
    this.table = table;
  }

  /** @return table */
  public String getTable() { return this.table; }
  /** @param table Table to set */
  public void setTable(final String table) { this.table = table; }

  @Override
  public void configure(final Config config) throws Exception {
    super.configure(config);
    if (getDriver() == null) { setDriver(DEFAULT_DRIVER); }
    if (getUrl() == null) { setUrl(DEFAULT_URL); }

    setTable(config.getProperty("table", DEFAULT_TABLE));
    LOGGER.config("[" + getName() + "] driver=" + getDriver());
    LOGGER.config("[" + getName() + "] table=" + getTable());
    // do not log url, it may contain user/pass
  }

  /**
   * After normal startup, check whether schema exists and attempt to create.
   */
  @Override
  public void startup() throws Exception {
    super.startup();
    // make sure schema exists
    if (!schemaExists()) {
      LOGGER.warning("[" + getName() + "] schema not found, creating");
      createSchema();
    }
  }

  /**
   * Check whether schema exists.
   *
   * @return boolean
   * @throws Exception if error occurs
   */
  public boolean schemaExists() throws Exception {
    final String sql = "select * from " + this.table + " limit 1";
    beginTransaction();
    try (final PreparedStatement test = getConnection().prepareStatement(sql)) {
      test.setQueryTimeout(60);
      // should throw exception if table does not exist
      try (final ResultSet rs = test.executeQuery()) {
        rs.next();
      }
      commitTransaction();
      // schema exists
      return true;
    } catch (Exception e) {
      rollbackTransaction();
      return false;
    }
  }

  /**
   * Attempt to create schema.
   *
   * Only supports sqlite or mysql.  When not using sqlite, relying on this
   * method is only recommended for local development.
   *
   * @throws Exception if error occurs
   */
  public void createSchema() throws Exception {
    // create schema
    beginTransaction();
    try (final Statement statement = getConnection().createStatement()) {
      String autoIncrement = "";
      if (getDriver().contains("mysql")) {
        autoIncrement = "AUTO_INCREMENT";
      }
      statement.executeUpdate(
          "CREATE TABLE " + this.table
          + " (id INTEGER PRIMARY KEY " + autoIncrement
          + ", created VARCHAR(255)"
          + ", name VARCHAR(255)"
          + ", data TEXT"
          + ")");
      statement.executeUpdate(
          "CREATE UNIQUE INDEX name_index ON " + this.table + " (name)");
      commitTransaction();
    } catch (Exception e) {
      rollbackTransaction();
      throw e;
    }
  }

  /**
   * Get tracking data for specified name.
   *
   * @param name
   *     name of tracking data.
   * @return null if data not found.
   * @throws Exception if error occurs
   */
  public synchronized JsonObject getTrackingData(final String name) throws Exception {
    JsonObject data = null;

    final String sql = "SELECT * FROM " + this.table + " WHERE name=?";
    beginTransaction();
    try (final PreparedStatement statement = getConnection().prepareStatement(sql)) {
      statement.setQueryTimeout(60);
      statement.setString(1, name);

      // execute and parse data
      try (final ResultSet rs = statement.executeQuery()) {
        while (rs.next()) {
          final String json = rs.getString("data");
          try (
            final JsonReader jsonReader = Json.createReader(
                new ByteArrayInputStream(json.getBytes()))
          ) {
            data = jsonReader.readObject();
          }
        }
      }
      commitTransaction();
    } catch (Exception e) {
      rollbackTransaction();
    }

    return data;
  }

  /**
   * Remove tracking data.
   *
   * @param name
   *     name of tracking data.
   * @throws Exception if error occurs
   */
  public synchronized void removeTrackingData(final String name) throws Exception {
    final String sql = "DELETE FROM " + this.table + " WHERE name=?";
    // create schema
    beginTransaction();
    try (final PreparedStatement statement = getConnection().prepareStatement(sql)) {
      statement.setQueryTimeout(60);
      statement.setString(1, name);

      statement.executeUpdate();
      commitTransaction();
    } catch (Exception e) {
      rollbackTransaction();
      throw e;
    }
  }

  /**
   * Add or update tracking data.
   *
   * @param name
   *     name of tracking data.
   * @param data
   *     data to store.
   * @throws Exception if error occurs
   */
  public synchronized void setTrackingData(final String name, final JsonObject data) throws Exception {
    final String update = "UPDATE " + this.table + " SET data=? WHERE name=?";
    // usually updated, try update first
    beginTransaction();
    try (final PreparedStatement updateStatement = getConnection().prepareStatement(update)) {
      updateStatement.setQueryTimeout(60);
      updateStatement.setString(1, data.toString());
      updateStatement.setString(2, name);
      // execute update
      final int count = updateStatement.executeUpdate();
      // check number of rows updated (whether row already exists)
      if (count == 0) {
        final String insert = "INSERT INTO " + this.table + " (data, name) VALUES (?, ?)";
        // no rows updated
        try (final PreparedStatement insertStatement = getConnection().prepareStatement(insert)) {
          insertStatement.setQueryTimeout(60);
          insertStatement.setString(1, data.toString());
          insertStatement.setString(2, name);
          // execute insert
          insertStatement.executeUpdate();
        }
      }
      commitTransaction();
    } catch (Exception e) {
      rollbackTransaction();
      throw e;
    }
  }

}