JDBCNotificationIndex.java

  1. /*
  2.  * JDBCNotificationIndex
  3.  */
  4. package gov.usgs.earthquake.distribution;

  5. import gov.usgs.earthquake.product.ProductId;
  6. import gov.usgs.earthquake.util.JDBCConnection;
  7. import gov.usgs.util.Config;
  8. import gov.usgs.util.StreamUtils;

  9. import java.io.File;
  10. import java.net.MalformedURLException;
  11. import java.net.URL;
  12. import java.sql.Connection;
  13. import java.sql.DriverManager;
  14. import java.sql.PreparedStatement;
  15. import java.sql.ResultSet;
  16. import java.sql.SQLException;
  17. import java.util.ArrayList;
  18. import java.util.Date;
  19. import java.util.Iterator;
  20. import java.util.List;
  21. import java.util.logging.Logger;

  22. /**
  23.  * Stores and retrieves Notifications.
  24.  *
  25.  * This is typically used by a NotificationReceiver to track its Notifications,
  26.  * but may also be used by NotificationListeners. Each object should maintain a
  27.  * separate NotificationIndex.
  28.  *
  29.  * This implementation uses a SQLite Database as the index.
  30.  *
  31.  * @see gov.usgs.earthquake.distribution.NotificationIndex
  32.  */
  33. public class JDBCNotificationIndex extends JDBCConnection implements
  34.         NotificationIndex {

  35.     private static final Logger LOGGER = Logger
  36.             .getLogger(JDBCNotificationIndex.class.getName());

  37.     /**
  38.      * Default (empty) DB Schema SQLite file for the index. If the configured
  39.      * index file does not exist in the file system at the time this instance's
  40.      * "startup()" method is called, then this file is copied out of the JAR
  41.      * file into the file system as configured.
  42.      *
  43.      * The schema contained in this file is very simple:
  44.      *
  45.      * CREATE TABLE notification_index ( id INTEGER PRIMARY KEY NOT NULL,
  46.      * product_source TEXT NOT NULL, product_type TEXT NOT NULL, product_code
  47.      * TEXT NOT NULL, product_update LONG NOT NULL, expiration_date LONG NOT
  48.      * NULL, tracker_url TEXT NOT NULL, product_url TEXT );
  49.      *
  50.      * CREATE TABLE tmp_lookup_table ( product_source TEXT, product_type TEXT,
  51.      * product_code TEXT);
  52.      *
  53.      * CREATE INDEX expired_index on notification_index (expiration_date);
  54.      * CREATE INDEX id_index on notification_index (product_source,
  55.      * product_type, product_code, product_update);
  56.      *
  57.      * CREATE TABLE notification_queue ( id INTEGER PRIMARY KEY NOT NULL,
  58.      * queue_name TEXT NOT NULL, product_source TEXT NOT NULL, product_type TEXT
  59.      * NOT NULL, product_code TEXT NOT NULL, product_update LONG NOT NULL );
  60.      *
  61.      * CREATE INDEX queue_index on notification_queue (queue_name,
  62.      * product_source, product_type, product_code, product_update);
  63.      *
  64.      */
  65.     private static final String JDBC_DB_SCHEMA = "etc/schema/notificationIndex.db";

  66.     // The following variables reference database information and are used for
  67.     // binding/fetching query parameters in the prepared statements
  68.     private static final String TABLE_NAME = "notification_index";
  69.     private static final String TMP_TABLE = "tmp_lookup_table";
  70.     private static final String ID_COLUMN = "id";
  71.     private static final String PRODUCT_SOURCE_COLUMN = "product_source";
  72.     private static final String PRODUCT_TYPE_COLUMN = "product_type";
  73.     private static final String PRODUCT_CODE_COLUMN = "product_code";
  74.     private static final String PRODUCT_UPDATE_COLUMN = "product_update";
  75.     private static final String EXPIRATION_DATE_COLUMN = "expiration_date";
  76.     private static final String TRACKER_URL_COLUMN = "tracker_url";
  77.     private static final String PRODUCT_URL_COLUMN = "product_url";

  78.     // SQLite driver information
  79.     /** SQLite driver class name. */
  80.     private static final String JDBC_DRIVER_CLASS = "org.sqlite.JDBC";
  81.     /** SQLite connect url without a filename. */
  82.     private static final String JDBC_CONNECT_URL = "jdbc:sqlite:";
  83.     /** Default SQLite database filename. */
  84.     private static final String JDBC_DEFAULT_FILE = "pd_index.db";

  85.     /**
  86.      * This is the property key used in the configuration file to specify a
  87.      * different SQLite database file. If this file doesn't exist it will be
  88.      * created at startup time
  89.      */
  90.     protected static final String JDBC_FILE_PROPERTY = "indexfile";

  91.     /** SQL stub for adding a notification to the index. */
  92.     private static final String DML_ADD_NOTIFICATION = String.format(
  93.             "INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s) VALUES "
  94.                     + "(?, ?, ?, ?, ?, ?, ?)", TABLE_NAME,
  95.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  96.             PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
  97.             PRODUCT_URL_COLUMN);

  98.     /** SQL stub for removing a notification from the index. */
  99.     private static final String DML_REMOVE_NOTIFICATION = String.format(
  100.             "DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ? "
  101.                     + "AND %s = ? AND %s = ? AND %s = ? AND %s = ?",
  102.             TABLE_NAME, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
  103.             PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  104.             TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN);

  105.     /** SQL stub for finding expired notifications. */
  106.     private static final String QUERY_FIND_EXPIRED_NOTIFICATIONS = String
  107.             .format("SELECT %s, %s, %s, %s, %s, %s, %s, %s FROM %s "
  108.                     + "WHERE %s <= ?", ID_COLUMN, PRODUCT_SOURCE_COLUMN,
  109.                     PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  110.                     PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  111.                     TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
  112.                     EXPIRATION_DATE_COLUMN);

  113.     /** SQL stub for finding notifications about a particular productId */
  114.     private static final String QUERY_FIND_NOTIFICATIONS_BY_ID = String.format(
  115.             "SELECT %s, %s, %s, %s, %s, %s, %s, %s FROM %s "
  116.                     + "WHERE %s = ? AND %s = ? AND %s = ? AND %s = ?",
  117.             ID_COLUMN, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
  118.             PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  119.             TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
  120.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  121.             PRODUCT_UPDATE_COLUMN);

  122.     /**
  123.      * SQL stub for finding notifications about products based on discrete data.
  124.      */
  125.     private static final String QUERY_FIND_NOTIFICATIONS_BY_DATA = String
  126.             .format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE "
  127.                     + "UPPER(%s) LIKE ? AND UPPER(%s) LIKE ? AND "
  128.                     + "UPPER(%s) LIKE ?", PRODUCT_SOURCE_COLUMN,
  129.                     PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  130.                     PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  131.                     TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
  132.                     PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
  133.                     PRODUCT_CODE_COLUMN);

  134.     /** Name of the SQLite DB file to use. This is configurable. */
  135.     private String _jdbc_index_file = null;

  136.     /**
  137.      * Connection URL. Created from the JDBC_CONNET_URL and configured index
  138.      * file.
  139.      */
  140.     private String _jdbc_connect_url = null;

  141.     // These are the prepared statements we will use for all DB interactions. //

  142.     private PreparedStatement _dml_addNotification = null;
  143.     private PreparedStatement _dml_removeNotification = null;

  144.     private PreparedStatement _query_findExpiredNotifications = null;
  145.     private PreparedStatement _query_findNotificationsById = null;
  146.     private PreparedStatement _query_findNotificationsByData = null;

  147.     // Stubs used in the list version of find by data method. //
  148.     private static final String DML_CREATE_TMP_TABLE = String.format(
  149.             "CREATE TABLE IF NOT EXISTS %s (%s TEXT, %s TEXT, %s TEXT)",
  150.             TMP_TABLE, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
  151.             PRODUCT_CODE_COLUMN);
  152.     private static final String DML_ADD_TMP_SOURCE = String.format(
  153.             "INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_SOURCE_COLUMN);
  154.     private static final String DML_ADD_TMP_TYPE = String.format(
  155.             "INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_TYPE_COLUMN);
  156.     private static final String DML_ADD_TMP_CODE = String.format(
  157.             "INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_CODE_COLUMN);

  158.     private static final String QUERY_SEARCH_BY_SOURCE_TYPE_CODE = String
  159.             .format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  160.                     + "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s) AND "
  161.                     + "%s IN (SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
  162.                     PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  163.                     PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  164.                     TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
  165.                     PRODUCT_SOURCE_COLUMN, PRODUCT_SOURCE_COLUMN, TMP_TABLE,
  166.                     PRODUCT_TYPE_COLUMN, PRODUCT_TYPE_COLUMN, TMP_TABLE,
  167.                     PRODUCT_CODE_COLUMN, PRODUCT_CODE_COLUMN, TMP_TABLE);

  168.     private static final String QUERY_SEARCH_BY_SOURCE_TYPE = String.format(
  169.             "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  170.                     + "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
  171.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  172.             PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
  173.             PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_SOURCE_COLUMN,
  174.             PRODUCT_SOURCE_COLUMN, TMP_TABLE, PRODUCT_TYPE_COLUMN,
  175.             PRODUCT_TYPE_COLUMN, TMP_TABLE);

  176.     private static final String QUERY_SEARCH_BY_SOURCE_CODE = String.format(
  177.             "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  178.                     + "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
  179.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  180.             PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
  181.             PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_SOURCE_COLUMN,
  182.             PRODUCT_SOURCE_COLUMN, TMP_TABLE, PRODUCT_CODE_COLUMN,
  183.             PRODUCT_CODE_COLUMN, TMP_TABLE);

  184.     private static final String QUERY_SEARCH_BY_TYPE_CODE = String.format(
  185.             "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  186.                     + "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
  187.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  188.             PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
  189.             PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_TYPE_COLUMN,
  190.             PRODUCT_TYPE_COLUMN, TMP_TABLE, PRODUCT_CODE_COLUMN,
  191.             PRODUCT_CODE_COLUMN, TMP_TABLE);

  192.     private static final String QUERY_SEARCH_BY_SOURCE = String
  193.             .format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  194.                     + "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
  195.                     PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  196.                     PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
  197.                     TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
  198.                     PRODUCT_SOURCE_COLUMN, PRODUCT_SOURCE_COLUMN, TMP_TABLE);

  199.     private static final String QUERY_SEARCH_BY_TYPE = String.format(
  200.             "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  201.                     + "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
  202.             PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
  203.             EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
  204.             TABLE_NAME, PRODUCT_TYPE_COLUMN, PRODUCT_TYPE_COLUMN, TMP_TABLE);

  205.     private static final String QUERY_SEARCH_BY_CODE = String.format(
  206.             "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
  207.                     + "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
  208.             PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
  209.             EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
  210.             TABLE_NAME, PRODUCT_CODE_COLUMN, PRODUCT_CODE_COLUMN, TMP_TABLE);
  211.     /*
  212.      * private static final String QUERY_ALL_NOTIFICATIONS = String.format(
  213.      * "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s", PRODUCT_SOURCE_COLUMN,
  214.      * PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
  215.      * EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
  216.      * TABLE_NAME);
  217.      */

  218.     private static final String QUERY_LATEST_NOTIFICATIONS = String.format(
  219.             "SELECT n.%s, n.%s, n.%s, n.%s, n.%s, n.%s, n.%s FROM %s n, "
  220.                     + "(select max(id) as id, product_source, product_type, "
  221.                     + "product_code, product_update from notification_index "
  222.                     + "group by product_source, product_type, product_code, "
  223.                     + "product_update) latest where n.id=latest.id "
  224.                     + " order by n.product_update asc",
  225.             PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
  226.             PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
  227.             PRODUCT_URL_COLUMN, TABLE_NAME);

  228.     // These are for searching the DB index by specific parameters. */
  229.     private PreparedStatement _dml_createTmpTable = null;
  230.     private PreparedStatement _dml_addTmpSource = null;
  231.     private PreparedStatement _dml_addTmpType = null;
  232.     private PreparedStatement _dml_addTmpCode = null;

  233.     private PreparedStatement _query_searchBySourceTypeCode = null;
  234.     private PreparedStatement _query_searchBySourceType = null;
  235.     private PreparedStatement _query_searchBySourceCode = null;
  236.     private PreparedStatement _query_searchByTypeCode = null;
  237.     private PreparedStatement _query_searchBySource = null;
  238.     private PreparedStatement _query_searchByType = null;
  239.     private PreparedStatement _query_searchByCode = null;
  240.     private PreparedStatement _query_getAllNotifications = null;

  241.     /**
  242.      * Default, no-arg constructor. This just ensures the JDBC SQLite driver is
  243.      * appropriately on the classpath for proper runtime execution. This
  244.      * probably will not get called directly in favor of the configurable
  245.      * constructor.
  246.      *
  247.      * @throws Exception
  248.      *             If the JDBC driver class is not found.
  249.      * @see #JDBC_DRIVER_CLASS
  250.      */
  251.     public JDBCNotificationIndex() throws Exception {
  252.         this((String) null);
  253.     }

  254.     /**
  255.      * Constructor call from filename, where filename is jdbc index file
  256.      * If null, then index file defaults
  257.      * @param filename String - What will be the index file
  258.      * @throws Exception if error occurs
  259.      */
  260.     public JDBCNotificationIndex(final String filename) throws Exception {
  261.         Class.forName(JDBC_DRIVER_CLASS);
  262.         _jdbc_index_file = filename;
  263.         if (_jdbc_index_file == null) {
  264.             _jdbc_index_file = JDBC_DEFAULT_FILE;
  265.         }
  266.         _jdbc_connect_url = JDBC_CONNECT_URL + _jdbc_index_file;
  267.     }

  268.     /**
  269.      * Constructor called from the config object conforming to the
  270.      * <code>Configurable</code> interface specification. This internally calls
  271.      * its no-arg constructor then configures itself.
  272.      *
  273.      * @param config
  274.      *            The config object from which this instance will be configured.
  275.      * @throws Exception
  276.      *             If the JDBC driver class is not found.
  277.      * @see gov.usgs.util.Configurable
  278.      * @see #JDBC_DRIVER_CLASS
  279.      */
  280.     public JDBCNotificationIndex(Config config) throws Exception {
  281.         this();
  282.         this.configure(config);
  283.     }

  284.     /**
  285.      * Reads the given <code>config</code> object and sets values appropriately.
  286.      *
  287.      * @param config
  288.      *            The config object from which this instance will be configured.
  289.      * @see gov.usgs.util.Configurable
  290.      */
  291.     public void configure(Config config) throws Exception {
  292.         _jdbc_index_file = config.getProperty(JDBC_FILE_PROPERTY);
  293.         if (_jdbc_index_file == null || "".equals(_jdbc_index_file)) {
  294.             _jdbc_index_file = JDBC_DEFAULT_FILE;
  295.         }
  296.         LOGGER.config("Notification index database is '" + _jdbc_index_file
  297.                 + "'");
  298.         _jdbc_connect_url = JDBC_CONNECT_URL + _jdbc_index_file;
  299.     }

  300.     @Override
  301.     protected Connection connect() throws Exception {
  302.         // Make sure file exists or copy it out of the JAR
  303.         File indexFile = new File(_jdbc_index_file);
  304.         if (!indexFile.exists()) {
  305.             // extract schema from jar
  306.             URL schemaURL = JDBCNotificationIndex.class.getClassLoader()
  307.                     .getResource(JDBC_DB_SCHEMA);
  308.             if (schemaURL == null) {
  309.                 schemaURL = new File(JDBC_DB_SCHEMA).toURI().toURL();
  310.             }
  311.             StreamUtils.transferStream(schemaURL, indexFile);
  312.         }

  313.         return DriverManager.getConnection(_jdbc_connect_url);
  314.     }

  315.     /**
  316.      * Connects to the JDBC DB index and prepares the DML/Query statements that
  317.      * will execute at runtime. If the JDBC DB index file does not exist then an
  318.      * empty schema will be copied out of the executing JAR file to be used.
  319.      *
  320.      * @see gov.usgs.util.Configurable
  321.      */
  322.     public void startup() throws Exception {
  323.         // call super startup to connect
  324.         super.startup();
  325.         Connection conn = getConnection();

  326.         // prepare statements
  327.         _dml_addNotification = conn.prepareStatement(DML_ADD_NOTIFICATION);
  328.         _dml_removeNotification = conn
  329.                 .prepareStatement(DML_REMOVE_NOTIFICATION);

  330.         _query_findExpiredNotifications = conn
  331.                 .prepareStatement(QUERY_FIND_EXPIRED_NOTIFICATIONS);
  332.         _query_findNotificationsById = conn
  333.                 .prepareStatement(QUERY_FIND_NOTIFICATIONS_BY_ID);
  334.         _query_findNotificationsByData = conn
  335.                 .prepareStatement(QUERY_FIND_NOTIFICATIONS_BY_DATA);

  336.         _dml_createTmpTable = conn.prepareStatement(DML_CREATE_TMP_TABLE);
  337.         _dml_addTmpSource = conn.prepareStatement(DML_ADD_TMP_SOURCE);
  338.         _dml_addTmpType = conn.prepareStatement(DML_ADD_TMP_TYPE);
  339.         _dml_addTmpCode = conn.prepareStatement(DML_ADD_TMP_CODE);

  340.         _query_searchBySourceTypeCode = conn
  341.                 .prepareStatement(QUERY_SEARCH_BY_SOURCE_TYPE_CODE);
  342.         _query_searchBySourceType = conn
  343.                 .prepareStatement(QUERY_SEARCH_BY_SOURCE_TYPE);
  344.         _query_searchBySourceCode = conn
  345.                 .prepareStatement(QUERY_SEARCH_BY_SOURCE_CODE);
  346.         _query_searchByTypeCode = conn
  347.                 .prepareStatement(QUERY_SEARCH_BY_TYPE_CODE);
  348.         _query_searchBySource = conn.prepareStatement(QUERY_SEARCH_BY_SOURCE);
  349.         _query_searchByType = conn.prepareStatement(QUERY_SEARCH_BY_TYPE);
  350.         _query_searchByCode = conn.prepareStatement(QUERY_SEARCH_BY_CODE);
  351.         _query_getAllNotifications = conn
  352.                 .prepareStatement(QUERY_LATEST_NOTIFICATIONS);

  353.     }

  354.     /**
  355.      * Closes the JDBC connection and all it's associated prepared statements.
  356.      *
  357.      * @see gov.usgs.util.Configurable
  358.      */
  359.     public synchronized void shutdown() throws Exception {
  360.         // Close the DML statements
  361.         try {
  362.             _dml_addNotification.close();
  363.         } catch (Exception e) {
  364.         } finally {
  365.             _dml_addNotification = null;
  366.         }
  367.         try {
  368.             _dml_removeNotification.close();
  369.         } catch (Exception e) {
  370.         } finally {
  371.             _dml_removeNotification = null;
  372.         }
  373.         try {
  374.             _dml_createTmpTable.close();
  375.         } catch (Exception e) {
  376.         } finally {
  377.             _dml_createTmpTable = null;
  378.         }
  379.         try {
  380.             _dml_addTmpSource.close();
  381.         } catch (Exception e) {
  382.         } finally {
  383.             _dml_addTmpSource = null;
  384.         }
  385.         try {
  386.             _dml_addTmpType.close();
  387.         } catch (Exception e) {
  388.         } finally {
  389.             _dml_addTmpType = null;
  390.         }
  391.         try {
  392.             _dml_addTmpCode.close();
  393.         } catch (Exception e) {
  394.         } finally {
  395.             _dml_addTmpCode = null;
  396.         }

  397.         // Close the query statements
  398.         try {
  399.             _query_findExpiredNotifications.close();
  400.         } catch (Exception e) {
  401.         } finally {
  402.             _query_findExpiredNotifications = null;
  403.         }
  404.         try {
  405.             _query_findNotificationsById.close();
  406.         } catch (Exception e) {
  407.         } finally {
  408.             _query_findNotificationsById = null;
  409.         }
  410.         try {
  411.             _query_findNotificationsByData.close();
  412.         } catch (Exception e) {
  413.         } finally {
  414.             _query_findNotificationsByData = null;
  415.         }
  416.         try {
  417.             _query_searchBySourceTypeCode.close();
  418.         } catch (Exception e) {
  419.         } finally {
  420.             _query_searchBySourceTypeCode = null;
  421.         }
  422.         try {
  423.             _query_searchBySourceType.close();
  424.         } catch (Exception e) {
  425.         } finally {
  426.             _query_searchBySourceType = null;
  427.         }
  428.         try {
  429.             _query_searchBySourceCode.close();
  430.         } catch (Exception e) {
  431.         } finally {
  432.             _query_searchBySourceCode = null;
  433.         }
  434.         try {
  435.             _query_searchByTypeCode.close();
  436.         } catch (Exception e) {
  437.         } finally {
  438.             _query_searchByTypeCode = null;
  439.         }
  440.         try {
  441.             _query_searchBySource.close();
  442.         } catch (Exception e) {
  443.         } finally {
  444.             _query_searchBySource = null;
  445.         }
  446.         try {
  447.             _query_searchByType.close();
  448.         } catch (Exception e) {
  449.         } finally {
  450.             _query_searchByType = null;
  451.         }
  452.         try {
  453.             _query_searchByCode.close();
  454.         } catch (Exception e) {
  455.         } finally {
  456.             _query_searchByCode = null;
  457.         }
  458.         try {
  459.             _query_getAllNotifications.close();
  460.         } catch (Exception e) {
  461.         } finally {
  462.             _query_getAllNotifications = null;
  463.         }

  464.         // call super shutdown to disconnect
  465.         super.shutdown();
  466.     }

  467.     /**
  468.      * Add a notification to the index.
  469.      *
  470.      * If an identical notification is already in the index, the implementation
  471.      * may choose whether or not to store the duplicate information.
  472.      *
  473.      * @param notification
  474.      *            the notification to add.
  475.      * @throws Exception
  476.      *             if an error occurs while storing the notification.
  477.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  478.      */
  479.     public synchronized void addNotification(Notification notification)
  480.             throws Exception {
  481.         // verify connection
  482.         this.verifyConnection();

  483.         // Read the product id from the notification
  484.         ProductId productId = notification.getProductId();

  485.         // Parse the update date from the product id
  486.         java.sql.Date updateDate = new java.sql.Date(productId.getUpdateTime()
  487.                 .getTime());

  488.         // Parse the expiration date from the notification
  489.         java.sql.Date expirationDate = new java.sql.Date(notification
  490.                 .getExpirationDate().getTime());

  491.         // Read the URL value from the notification
  492.         String trackerUrl = notification.getTrackerURL() != null
  493.                 ? notification.getTrackerURL().toString()
  494.                 : "";

  495.         // Set the values we parsed above
  496.         _dml_addNotification.setString(1, productId.getSource());
  497.         _dml_addNotification.setString(2, productId.getType());
  498.         _dml_addNotification.setString(3, productId.getCode());
  499.         _dml_addNotification.setDate(4, updateDate);
  500.         _dml_addNotification.setDate(5, expirationDate);
  501.         _dml_addNotification.setString(6, trackerUrl);

  502.         // If this is a URL notification, set the product URL value as well
  503.         if (notification instanceof URLNotification) {
  504.             String productUrl = ((URLNotification) notification)
  505.                     .getProductURL().toString();
  506.             _dml_addNotification.setString(7, productUrl);
  507.         } else {
  508.             _dml_addNotification.setString(7, "");
  509.         }

  510.         // already verified above
  511.         Connection conn = getConnection();
  512.         try {
  513.             // Begin a transaction
  514.             conn.setAutoCommit(false);
  515.             // Execute the query
  516.             _dml_addNotification.executeUpdate();
  517.             // Commit the changes
  518.             conn.setAutoCommit(true);
  519.         } catch (SQLException sqx) {
  520.             // Undo any changes that may be in an unknown state. Ignore
  521.             // exceptions that occur in this call since we're already throwing
  522.             // an exception
  523.             try {
  524.                 conn.rollback();
  525.             } catch (SQLException ex) {
  526.             }

  527.             // Re-throw this exception
  528.             throw sqx;
  529.         } finally {
  530.             conn.setAutoCommit(true);
  531.         }
  532.     }

  533.     /**
  534.      * Remove a notification from the index.
  535.      *
  536.      * All matching notifications should be removed from the index.
  537.      *
  538.      * @param notification
  539.      *            the notification to remove.
  540.      * @throws Exception
  541.      *             if an error occurs while removing the notification.
  542.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  543.      */
  544.     public synchronized void removeNotification(Notification notification)
  545.             throws Exception {
  546.         // verify connection
  547.         this.verifyConnection();

  548.         // Read the product id from the notification
  549.         ProductId productId = notification.getProductId();
  550.         // Parse the update date from the product id
  551.         java.sql.Date updateDate = new java.sql.Date(productId.getUpdateTime()
  552.                 .getTime());
  553.         // Parse the expiration date from the notification
  554.         java.sql.Date expirationDate = new java.sql.Date(notification
  555.                 .getExpirationDate().getTime());
  556.         // Read the URL value from the notification
  557.         String trackerUrl = notification.getTrackerURL() != null
  558.                 ? notification.getTrackerURL().toString()
  559.                 : "";

  560.         // Set the values we parsed above
  561.         _dml_removeNotification.setString(1, productId.getSource());
  562.         _dml_removeNotification.setString(2, productId.getType());
  563.         _dml_removeNotification.setString(3, productId.getCode());
  564.         _dml_removeNotification.setDate(4, updateDate);
  565.         _dml_removeNotification.setDate(5, expirationDate);
  566.         _dml_removeNotification.setString(6, trackerUrl);

  567.         // If this is a URL notification, set the product URL value as well
  568.         if (notification instanceof URLNotification) {
  569.             String productUrl = ((URLNotification) notification)
  570.                     .getProductURL().toString();
  571.             _dml_removeNotification.setString(7, productUrl);
  572.         } else {
  573.             // _dml_removeNotification.setNull(7, java.sql.Types.VARCHAR);
  574.             _dml_removeNotification.setString(7, "");
  575.         }

  576.         // already verified above
  577.         Connection conn = getConnection();
  578.         try {
  579.             // Begin a transaction
  580.             conn.setAutoCommit(false);
  581.             // Execute the query
  582.             _dml_removeNotification.executeUpdate();
  583.             // Commit the changes
  584.             conn.setAutoCommit(true);
  585.         } catch (SQLException sqx) {
  586.             // Undo any changes that may be in an unknown state. Ignore
  587.             // exceptions that occur in this call since we're already throwing
  588.             // an exception
  589.             try {
  590.                 conn.rollback();
  591.             } catch (SQLException ex) {
  592.             }
  593.             // Re-throw this exception
  594.             throw sqx;
  595.         } finally {
  596.             conn.setAutoCommit(true);
  597.         }
  598.     }

  599.     /**
  600.      * Remove notifications from the index.
  601.      *
  602.      * All matching notifications should be removed from the index.
  603.      *
  604.      * @param notifications
  605.      *            the notifications to remove.
  606.      * @throws Exception
  607.      *             if an error occurs while removing the notifications.
  608.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  609.      */
  610.     public synchronized void removeNotifications(List<Notification> notifications)
  611.             throws Exception {
  612.         for (Notification notification : notifications) {
  613.             this.removeNotification(notification);
  614.         }
  615.     }

  616.     /**
  617.      * Search the index for notifications matching id.
  618.      *
  619.      * If more than one notification matches, all should be returned.
  620.      *
  621.      * @param id
  622.      *            the ProductId to find.
  623.      * @return a list of matching notifications.
  624.      * @throws Exception
  625.      *             if an error occurs while searching the index.
  626.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  627.      */
  628.     public synchronized List<Notification> findNotifications(ProductId id)
  629.             throws Exception {
  630.         // verify connection
  631.         this.verifyConnection();

  632.         String source = id.getSource();
  633.         String type = id.getType();
  634.         String code = id.getCode();
  635.         java.sql.Date update = new java.sql.Date(id.getUpdateTime().getTime());

  636.         _query_findNotificationsById.setString(1, source);
  637.         _query_findNotificationsById.setString(2, type);
  638.         _query_findNotificationsById.setString(3, code);
  639.         _query_findNotificationsById.setDate(4, update);

  640.         return getNotifications(_query_findNotificationsById);
  641.     }

  642.     /**
  643.      * Search the index for notifications matching the sources, types, and
  644.      * codes.
  645.      *
  646.      * Only one notification for each unique ProductId
  647.      * (source+type+code+updateTime) should be returned. If sources, types,
  648.      * and/or codes are null, that parameter should be considered a wildcard. If
  649.      * sources, types, and codes are all null, a notification for each unique
  650.      * ProductId in the index should be returned.
  651.      *
  652.      * @param source
  653.      *            sources to include, or all if null.
  654.      * @param type
  655.      *            types to include, or all if null.
  656.      * @param code
  657.      *            codes to include, or all if null.
  658.      * @return a list of matching notifications.
  659.      * @throws Exception
  660.      *             if an error occurs while searching the index.
  661.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  662.      */
  663.     public synchronized List<Notification> findNotifications(String source,
  664.             String type, String code) throws Exception {
  665.         // verify connection
  666.         this.verifyConnection();

  667.         source = (source == null) ? "%" : source.toUpperCase();
  668.         type = (type == null) ? "%" : type.toUpperCase();
  669.         code = (code == null) ? "%" : code.toUpperCase();

  670.         _query_findNotificationsByData.setString(1, source);
  671.         _query_findNotificationsByData.setString(2, type);
  672.         _query_findNotificationsByData.setString(3, code);

  673.         return getNotifications(_query_findNotificationsByData);
  674.     }

  675.     /**
  676.      * Search the index for notifications matching the sources, types, and
  677.      * codes.
  678.      *
  679.      * Only one notification for each unique ProductId
  680.      * (source+type+code+updateTime) should be returned. If sources, types,
  681.      * and/or codes are null, that parameter should be considered a wildcard. If
  682.      * sources, types, and codes are all null, a notification for each unique
  683.      * ProductId in the index should be returned.
  684.      *
  685.      * This implementation require synchronization to prevent SQLExceptions
  686.      * caused by concurrent access. SQLite locks the database whenever there is
  687.      * an open ResultSet resource. So even read queries can end up causing SQL
  688.      * concurrent access problems.
  689.      *
  690.      * @param sources
  691.      *            sources to include, or all if null.
  692.      * @param types
  693.      *            types to include, or all if null.
  694.      * @param codes
  695.      *            codes to include, or all if null.
  696.      * @return a list of matching notifications.
  697.      * @throws Exception
  698.      *             if an error occurs while searching the index.
  699.      */
  700.     public synchronized List<Notification> findNotifications(
  701.             List<String> sources, List<String> types, List<String> codes)
  702.             throws Exception {
  703.         // verify connection
  704.         this.verifyConnection();

  705.         List<Notification> n = null;

  706.         Connection conn = getConnection();
  707.         try {
  708.             // begin a transaction
  709.             conn.setAutoCommit(false);

  710.             // Create a temporary lookup table
  711.             _dml_createTmpTable.executeUpdate();

  712.             // Populate the temporary lookup table with our given lists
  713.             if (sources != null) {
  714.                 // Not null, insert values
  715.                 Iterator<String> iter = sources.iterator();
  716.                 while (iter.hasNext()) {
  717.                     _dml_addTmpSource.setString(1, iter.next());
  718.                     _dml_addTmpSource.addBatch();
  719.                 }
  720.                 _dml_addTmpSource.executeBatch();
  721.             }

  722.             if (types != null) {
  723.                 // Not null, insert values
  724.                 Iterator<String> iter = types.iterator();
  725.                 while (iter.hasNext()) {
  726.                     _dml_addTmpType.setString(1, iter.next());
  727.                     _dml_addTmpType.addBatch();
  728.                 }
  729.                 _dml_addTmpType.executeBatch();
  730.             }

  731.             if (codes != null) {
  732.                 // Not null, insert values
  733.                 Iterator<String> iter = codes.iterator();
  734.                 while (iter.hasNext()) {
  735.                     _dml_addTmpCode.setString(1, iter.next());
  736.                     _dml_addTmpCode.addBatch();
  737.                 }
  738.                 _dml_addTmpCode.executeBatch();
  739.             }

  740.             // is this a problem? reading with uncommitted writes?
  741.             PreparedStatement ps = getCorrectStatement(sources, types, codes);
  742.             n = getNotifications(ps);
  743.         } finally {
  744.             conn.rollback();
  745.             // todo: this looks funky, but it's re-enabling autoCommit, which is
  746.             // needed for selects to not block other transactions
  747.             conn.setAutoCommit(true);
  748.         }

  749.         return n;
  750.     }

  751.     /**
  752.      * Search the index for expired notifications.
  753.      *
  754.      * All expired notifications, even if duplicate, should be returned.
  755.      *
  756.      * @return a list of expired notifications.
  757.      * @throws Exception
  758.      *             if an error occurs while searching the index.
  759.      * @see gov.usgs.earthquake.distribution.NotificationIndex
  760.      */
  761.     public synchronized List<Notification> findExpiredNotifications()
  762.             throws Exception {
  763.         // verify connection
  764.         this.verifyConnection();

  765.         // Create a new calendar object set to current date/time
  766.         java.sql.Date curDate = new java.sql.Date((new Date()).getTime());

  767.         // Bind the expiration date parameter and run the query
  768.         _query_findExpiredNotifications.setDate(1, curDate);

  769.         return getNotifications(_query_findExpiredNotifications);
  770.     }

  771.     /**
  772.      * Executes a prepared statement and parses the result set into a list of
  773.      * notifications. The prepared statement can have any set of criteria and
  774.      * all required parameters should be bound before calling this method. The
  775.      * result set of the prepared statement must include at least: -
  776.      * PRODUCT_SOURCE_COLUMN<br>
  777.      * - PRODUCT_TYPE_COLUMN<br>
  778.      * - PRODUCT_CODE_COLUMN<br>
  779.      * - PRODUCT_UPDATE_COLUMN<br>
  780.      * - EXPIRATION_DATE_COLUMN<br>
  781.      * - TRACKER_URL_COLUMN<br>
  782.      * - PRODUCT_URL_COLUMN<br>
  783.      *
  784.      * @param ps
  785.      *            The prepared statement to execute.
  786.      * @return A list of notifications returned by executing the statement.
  787.      * @throws Exception
  788.      *             If a <code>SQLException</code> occurs.
  789.      */
  790.     protected synchronized List<Notification> getNotifications(PreparedStatement ps)
  791.             throws Exception {
  792.         List<Notification> n = new ArrayList<Notification>();
  793.         ResultSet rs = null;

  794.         try {
  795.             rs = ps.executeQuery();
  796.             while (rs.next()) {
  797.                 n.add(parseNotification(rs.getString(PRODUCT_SOURCE_COLUMN),
  798.                         rs.getString(PRODUCT_TYPE_COLUMN),
  799.                         rs.getString(PRODUCT_CODE_COLUMN),
  800.                         rs.getDate(PRODUCT_UPDATE_COLUMN),
  801.                         rs.getDate(EXPIRATION_DATE_COLUMN),
  802.                         rs.getString(TRACKER_URL_COLUMN),
  803.                         rs.getString(PRODUCT_URL_COLUMN)));
  804.             }
  805.         } finally {
  806.             try {
  807.                 rs.close();
  808.             } catch (Exception e) {
  809.                 //ignore
  810.             }
  811.         }

  812.         return n;
  813.     }

  814.     /**
  815.      * Creates and returns a <code>Notification</code> based on the provided
  816.      * data. If the <code>download</code> string references a valid URL, then a
  817.      * <code>URLNotification</code> is created, otherwise a
  818.      * <code>DefaultNotification</code> is created.
  819.      *
  820.      * @param source
  821.      *            The product source string.
  822.      * @param type
  823.      *            The product type string.
  824.      * @param code
  825.      *            The product code string.
  826.      * @param update
  827.      *            The latest update date/time for the product.
  828.      * @param expires
  829.      *            The date/time when this notification expires.
  830.      * @param tracker
  831.      *            A reference to a URL where information about this product is
  832.      *            posted.
  833.      * @param download
  834.      *            A reference to a URL where one can download this product, or
  835.      *            <code>null</code> if this is not a
  836.      *            <code>URLNotification</code>.
  837.      *
  838.      * @return The generated notification, or <code>null</code> if one could not
  839.      *         be created (but an exception did not occur).
  840.      *
  841.      * @throws Exception
  842.      *             If the <code>tracker</code> string cannot be successfully
  843.      *             parsed into a valid URL.
  844.      */
  845.     protected Notification parseNotification(String source, String type,
  846.             String code, java.sql.Date update, java.sql.Date expires,
  847.             String tracker, String download) throws Exception {
  848.         Notification n = null;
  849.         ProductId productId = new ProductId(source, type, code, update);
  850.         URL trackerURL = !"".equals(tracker) ? new URL(tracker) : null;
  851.         try {
  852.             n = new URLNotification(productId, expires, trackerURL, new URL(download));
  853.         } catch (MalformedURLException mux) {
  854.             n = new DefaultNotification(productId, expires, trackerURL);
  855.         }
  856.         return n;
  857.     }

  858.     /**
  859.      * @param sources List string of sources
  860.      * @param types List string of types
  861.      * @param codes List string of codes
  862.      * @return prepared query based on what is/is not null
  863.      * @throws Exception if error occurs
  864.      */
  865.     protected PreparedStatement getCorrectStatement(List<String> sources,
  866.             List<String> types, List<String> codes) throws Exception {
  867.         if (sources != null && types != null && codes != null) {
  868.             return _query_searchBySourceTypeCode;
  869.         } else if (sources != null && types != null && codes == null) {
  870.             return _query_searchBySourceType;
  871.         } else if (sources != null && types == null && codes != null) {
  872.             return _query_searchBySourceCode;
  873.         } else if (sources == null && types != null && codes != null) {
  874.             return _query_searchByTypeCode;
  875.         } else if (sources != null && types == null && codes == null) {
  876.             return _query_searchBySource;
  877.         } else if (sources == null && types != null && codes == null) {
  878.             return _query_searchByType;
  879.         } else if (sources == null && types == null && codes != null) {
  880.             return _query_searchByCode;
  881.         } else if (sources == null && types == null && codes == null) {
  882.             return _query_getAllNotifications;
  883.         }

  884.         return null;
  885.     }

  886. }