JDBCNotificationIndex.java
/*
* JDBCNotificationIndex
*/
package gov.usgs.earthquake.distribution;
import gov.usgs.earthquake.product.ProductId;
import gov.usgs.earthquake.util.JDBCConnection;
import gov.usgs.util.Config;
import gov.usgs.util.StreamUtils;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
/**
* Stores and retrieves Notifications.
*
* This is typically used by a NotificationReceiver to track its Notifications,
* but may also be used by NotificationListeners. Each object should maintain a
* separate NotificationIndex.
*
* This implementation uses a SQLite Database as the index.
*
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public class JDBCNotificationIndex extends JDBCConnection implements
NotificationIndex {
private static final Logger LOGGER = Logger
.getLogger(JDBCNotificationIndex.class.getName());
/**
* Default (empty) DB Schema SQLite file for the index. If the configured
* index file does not exist in the file system at the time this instance's
* "startup()" method is called, then this file is copied out of the JAR
* file into the file system as configured.
*
* The schema contained in this file is very simple:
*
* CREATE TABLE notification_index ( id INTEGER PRIMARY KEY NOT NULL,
* product_source TEXT NOT NULL, product_type TEXT NOT NULL, product_code
* TEXT NOT NULL, product_update LONG NOT NULL, expiration_date LONG NOT
* NULL, tracker_url TEXT NOT NULL, product_url TEXT );
*
* CREATE TABLE tmp_lookup_table ( product_source TEXT, product_type TEXT,
* product_code TEXT);
*
* CREATE INDEX expired_index on notification_index (expiration_date);
* CREATE INDEX id_index on notification_index (product_source,
* product_type, product_code, product_update);
*
* CREATE TABLE notification_queue ( id INTEGER PRIMARY KEY NOT NULL,
* queue_name TEXT NOT NULL, product_source TEXT NOT NULL, product_type TEXT
* NOT NULL, product_code TEXT NOT NULL, product_update LONG NOT NULL );
*
* CREATE INDEX queue_index on notification_queue (queue_name,
* product_source, product_type, product_code, product_update);
*
*/
private static final String JDBC_DB_SCHEMA = "etc/schema/notificationIndex.db";
// The following variables reference database information and are used for
// binding/fetching query parameters in the prepared statements
private static final String TABLE_NAME = "notification_index";
private static final String TMP_TABLE = "tmp_lookup_table";
private static final String ID_COLUMN = "id";
private static final String PRODUCT_SOURCE_COLUMN = "product_source";
private static final String PRODUCT_TYPE_COLUMN = "product_type";
private static final String PRODUCT_CODE_COLUMN = "product_code";
private static final String PRODUCT_UPDATE_COLUMN = "product_update";
private static final String EXPIRATION_DATE_COLUMN = "expiration_date";
private static final String TRACKER_URL_COLUMN = "tracker_url";
private static final String PRODUCT_URL_COLUMN = "product_url";
// SQLite driver information
/** SQLite driver class name. */
private static final String JDBC_DRIVER_CLASS = "org.sqlite.JDBC";
/** SQLite connect url without a filename. */
private static final String JDBC_CONNECT_URL = "jdbc:sqlite:";
/** Default SQLite database filename. */
private static final String JDBC_DEFAULT_FILE = "pd_index.db";
/**
* This is the property key used in the configuration file to specify a
* different SQLite database file. If this file doesn't exist it will be
* created at startup time
*/
protected static final String JDBC_FILE_PROPERTY = "indexfile";
/** SQL stub for adding a notification to the index. */
private static final String DML_ADD_NOTIFICATION = String.format(
"INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s) VALUES "
+ "(?, ?, ?, ?, ?, ?, ?)", TABLE_NAME,
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
PRODUCT_URL_COLUMN);
/** SQL stub for removing a notification from the index. */
private static final String DML_REMOVE_NOTIFICATION = String.format(
"DELETE FROM %s WHERE %s = ? AND %s = ? AND %s = ? "
+ "AND %s = ? AND %s = ? AND %s = ? AND %s = ?",
TABLE_NAME, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN);
/** SQL stub for finding expired notifications. */
private static final String QUERY_FIND_EXPIRED_NOTIFICATIONS = String
.format("SELECT %s, %s, %s, %s, %s, %s, %s, %s FROM %s "
+ "WHERE %s <= ?", ID_COLUMN, PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
EXPIRATION_DATE_COLUMN);
/** SQL stub for finding notifications about a particular productId */
private static final String QUERY_FIND_NOTIFICATIONS_BY_ID = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s, %s FROM %s "
+ "WHERE %s = ? AND %s = ? AND %s = ? AND %s = ?",
ID_COLUMN, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN);
/**
* SQL stub for finding notifications about products based on discrete data.
*/
private static final String QUERY_FIND_NOTIFICATIONS_BY_DATA = String
.format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE "
+ "UPPER(%s) LIKE ? AND UPPER(%s) LIKE ? AND "
+ "UPPER(%s) LIKE ?", PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
PRODUCT_CODE_COLUMN);
/** Name of the SQLite DB file to use. This is configurable. */
private String _jdbc_index_file = null;
/**
* Connection URL. Created from the JDBC_CONNET_URL and configured index
* file.
*/
private String _jdbc_connect_url = null;
// These are the prepared statements we will use for all DB interactions. //
private PreparedStatement _dml_addNotification = null;
private PreparedStatement _dml_removeNotification = null;
private PreparedStatement _query_findExpiredNotifications = null;
private PreparedStatement _query_findNotificationsById = null;
private PreparedStatement _query_findNotificationsByData = null;
// Stubs used in the list version of find by data method. //
private static final String DML_CREATE_TMP_TABLE = String.format(
"CREATE TABLE IF NOT EXISTS %s (%s TEXT, %s TEXT, %s TEXT)",
TMP_TABLE, PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN,
PRODUCT_CODE_COLUMN);
private static final String DML_ADD_TMP_SOURCE = String.format(
"INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_SOURCE_COLUMN);
private static final String DML_ADD_TMP_TYPE = String.format(
"INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_TYPE_COLUMN);
private static final String DML_ADD_TMP_CODE = String.format(
"INSERT INTO %s (%s) VALUES (?)", TMP_TABLE, PRODUCT_CODE_COLUMN);
private static final String QUERY_SEARCH_BY_SOURCE_TYPE_CODE = String
.format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s) AND "
+ "%s IN (SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
PRODUCT_SOURCE_COLUMN, PRODUCT_SOURCE_COLUMN, TMP_TABLE,
PRODUCT_TYPE_COLUMN, PRODUCT_TYPE_COLUMN, TMP_TABLE,
PRODUCT_CODE_COLUMN, PRODUCT_CODE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_SOURCE_TYPE = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_SOURCE_COLUMN,
PRODUCT_SOURCE_COLUMN, TMP_TABLE, PRODUCT_TYPE_COLUMN,
PRODUCT_TYPE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_SOURCE_CODE = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_SOURCE_COLUMN,
PRODUCT_SOURCE_COLUMN, TMP_TABLE, PRODUCT_CODE_COLUMN,
PRODUCT_CODE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_TYPE_CODE = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s) AND %s IN (SELECT %s FROM %s)",
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
PRODUCT_URL_COLUMN, TABLE_NAME, PRODUCT_TYPE_COLUMN,
PRODUCT_TYPE_COLUMN, TMP_TABLE, PRODUCT_CODE_COLUMN,
PRODUCT_CODE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_SOURCE = String
.format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN,
TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN, TABLE_NAME,
PRODUCT_SOURCE_COLUMN, PRODUCT_SOURCE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_TYPE = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
TABLE_NAME, PRODUCT_TYPE_COLUMN, PRODUCT_TYPE_COLUMN, TMP_TABLE);
private static final String QUERY_SEARCH_BY_CODE = String.format(
"SELECT %s, %s, %s, %s, %s, %s, %s FROM %s WHERE %s IN "
+ "(SELECT %s FROM %s)", PRODUCT_SOURCE_COLUMN,
PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
TABLE_NAME, PRODUCT_CODE_COLUMN, PRODUCT_CODE_COLUMN, TMP_TABLE);
/*
* private static final String QUERY_ALL_NOTIFICATIONS = String.format(
* "SELECT %s, %s, %s, %s, %s, %s, %s FROM %s", PRODUCT_SOURCE_COLUMN,
* PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN, PRODUCT_UPDATE_COLUMN,
* EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN, PRODUCT_URL_COLUMN,
* TABLE_NAME);
*/
private static final String QUERY_LATEST_NOTIFICATIONS = String.format(
"SELECT n.%s, n.%s, n.%s, n.%s, n.%s, n.%s, n.%s FROM %s n, "
+ "(select max(id) as id, product_source, product_type, "
+ "product_code, product_update from notification_index "
+ "group by product_source, product_type, product_code, "
+ "product_update) latest where n.id=latest.id "
+ " order by n.product_update asc",
PRODUCT_SOURCE_COLUMN, PRODUCT_TYPE_COLUMN, PRODUCT_CODE_COLUMN,
PRODUCT_UPDATE_COLUMN, EXPIRATION_DATE_COLUMN, TRACKER_URL_COLUMN,
PRODUCT_URL_COLUMN, TABLE_NAME);
// These are for searching the DB index by specific parameters. */
private PreparedStatement _dml_createTmpTable = null;
private PreparedStatement _dml_addTmpSource = null;
private PreparedStatement _dml_addTmpType = null;
private PreparedStatement _dml_addTmpCode = null;
private PreparedStatement _query_searchBySourceTypeCode = null;
private PreparedStatement _query_searchBySourceType = null;
private PreparedStatement _query_searchBySourceCode = null;
private PreparedStatement _query_searchByTypeCode = null;
private PreparedStatement _query_searchBySource = null;
private PreparedStatement _query_searchByType = null;
private PreparedStatement _query_searchByCode = null;
private PreparedStatement _query_getAllNotifications = null;
/**
* Default, no-arg constructor. This just ensures the JDBC SQLite driver is
* appropriately on the classpath for proper runtime execution. This
* probably will not get called directly in favor of the configurable
* constructor.
*
* @throws Exception
* If the JDBC driver class is not found.
* @see #JDBC_DRIVER_CLASS
*/
public JDBCNotificationIndex() throws Exception {
this((String) null);
}
/**
* Constructor call from filename, where filename is jdbc index file
* If null, then index file defaults
* @param filename String - What will be the index file
* @throws Exception if error occurs
*/
public JDBCNotificationIndex(final String filename) throws Exception {
Class.forName(JDBC_DRIVER_CLASS);
_jdbc_index_file = filename;
if (_jdbc_index_file == null) {
_jdbc_index_file = JDBC_DEFAULT_FILE;
}
_jdbc_connect_url = JDBC_CONNECT_URL + _jdbc_index_file;
}
/**
* Constructor called from the config object conforming to the
* <code>Configurable</code> interface specification. This internally calls
* its no-arg constructor then configures itself.
*
* @param config
* The config object from which this instance will be configured.
* @throws Exception
* If the JDBC driver class is not found.
* @see gov.usgs.util.Configurable
* @see #JDBC_DRIVER_CLASS
*/
public JDBCNotificationIndex(Config config) throws Exception {
this();
this.configure(config);
}
/**
* Reads the given <code>config</code> object and sets values appropriately.
*
* @param config
* The config object from which this instance will be configured.
* @see gov.usgs.util.Configurable
*/
public void configure(Config config) throws Exception {
_jdbc_index_file = config.getProperty(JDBC_FILE_PROPERTY);
if (_jdbc_index_file == null || "".equals(_jdbc_index_file)) {
_jdbc_index_file = JDBC_DEFAULT_FILE;
}
LOGGER.config("Notification index database is '" + _jdbc_index_file
+ "'");
_jdbc_connect_url = JDBC_CONNECT_URL + _jdbc_index_file;
}
@Override
protected Connection connect() throws Exception {
// Make sure file exists or copy it out of the JAR
File indexFile = new File(_jdbc_index_file);
if (!indexFile.exists()) {
// extract schema from jar
URL schemaURL = JDBCNotificationIndex.class.getClassLoader()
.getResource(JDBC_DB_SCHEMA);
if (schemaURL == null) {
schemaURL = new File(JDBC_DB_SCHEMA).toURI().toURL();
}
StreamUtils.transferStream(schemaURL, indexFile);
}
return DriverManager.getConnection(_jdbc_connect_url);
}
/**
* Connects to the JDBC DB index and prepares the DML/Query statements that
* will execute at runtime. If the JDBC DB index file does not exist then an
* empty schema will be copied out of the executing JAR file to be used.
*
* @see gov.usgs.util.Configurable
*/
public void startup() throws Exception {
// call super startup to connect
super.startup();
Connection conn = getConnection();
// prepare statements
_dml_addNotification = conn.prepareStatement(DML_ADD_NOTIFICATION);
_dml_removeNotification = conn
.prepareStatement(DML_REMOVE_NOTIFICATION);
_query_findExpiredNotifications = conn
.prepareStatement(QUERY_FIND_EXPIRED_NOTIFICATIONS);
_query_findNotificationsById = conn
.prepareStatement(QUERY_FIND_NOTIFICATIONS_BY_ID);
_query_findNotificationsByData = conn
.prepareStatement(QUERY_FIND_NOTIFICATIONS_BY_DATA);
_dml_createTmpTable = conn.prepareStatement(DML_CREATE_TMP_TABLE);
_dml_addTmpSource = conn.prepareStatement(DML_ADD_TMP_SOURCE);
_dml_addTmpType = conn.prepareStatement(DML_ADD_TMP_TYPE);
_dml_addTmpCode = conn.prepareStatement(DML_ADD_TMP_CODE);
_query_searchBySourceTypeCode = conn
.prepareStatement(QUERY_SEARCH_BY_SOURCE_TYPE_CODE);
_query_searchBySourceType = conn
.prepareStatement(QUERY_SEARCH_BY_SOURCE_TYPE);
_query_searchBySourceCode = conn
.prepareStatement(QUERY_SEARCH_BY_SOURCE_CODE);
_query_searchByTypeCode = conn
.prepareStatement(QUERY_SEARCH_BY_TYPE_CODE);
_query_searchBySource = conn.prepareStatement(QUERY_SEARCH_BY_SOURCE);
_query_searchByType = conn.prepareStatement(QUERY_SEARCH_BY_TYPE);
_query_searchByCode = conn.prepareStatement(QUERY_SEARCH_BY_CODE);
_query_getAllNotifications = conn
.prepareStatement(QUERY_LATEST_NOTIFICATIONS);
}
/**
* Closes the JDBC connection and all it's associated prepared statements.
*
* @see gov.usgs.util.Configurable
*/
public synchronized void shutdown() throws Exception {
// Close the DML statements
try {
_dml_addNotification.close();
} catch (Exception e) {
} finally {
_dml_addNotification = null;
}
try {
_dml_removeNotification.close();
} catch (Exception e) {
} finally {
_dml_removeNotification = null;
}
try {
_dml_createTmpTable.close();
} catch (Exception e) {
} finally {
_dml_createTmpTable = null;
}
try {
_dml_addTmpSource.close();
} catch (Exception e) {
} finally {
_dml_addTmpSource = null;
}
try {
_dml_addTmpType.close();
} catch (Exception e) {
} finally {
_dml_addTmpType = null;
}
try {
_dml_addTmpCode.close();
} catch (Exception e) {
} finally {
_dml_addTmpCode = null;
}
// Close the query statements
try {
_query_findExpiredNotifications.close();
} catch (Exception e) {
} finally {
_query_findExpiredNotifications = null;
}
try {
_query_findNotificationsById.close();
} catch (Exception e) {
} finally {
_query_findNotificationsById = null;
}
try {
_query_findNotificationsByData.close();
} catch (Exception e) {
} finally {
_query_findNotificationsByData = null;
}
try {
_query_searchBySourceTypeCode.close();
} catch (Exception e) {
} finally {
_query_searchBySourceTypeCode = null;
}
try {
_query_searchBySourceType.close();
} catch (Exception e) {
} finally {
_query_searchBySourceType = null;
}
try {
_query_searchBySourceCode.close();
} catch (Exception e) {
} finally {
_query_searchBySourceCode = null;
}
try {
_query_searchByTypeCode.close();
} catch (Exception e) {
} finally {
_query_searchByTypeCode = null;
}
try {
_query_searchBySource.close();
} catch (Exception e) {
} finally {
_query_searchBySource = null;
}
try {
_query_searchByType.close();
} catch (Exception e) {
} finally {
_query_searchByType = null;
}
try {
_query_searchByCode.close();
} catch (Exception e) {
} finally {
_query_searchByCode = null;
}
try {
_query_getAllNotifications.close();
} catch (Exception e) {
} finally {
_query_getAllNotifications = null;
}
// call super shutdown to disconnect
super.shutdown();
}
/**
* Add a notification to the index.
*
* If an identical notification is already in the index, the implementation
* may choose whether or not to store the duplicate information.
*
* @param notification
* the notification to add.
* @throws Exception
* if an error occurs while storing the notification.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized void addNotification(Notification notification)
throws Exception {
// verify connection
this.verifyConnection();
// Read the product id from the notification
ProductId productId = notification.getProductId();
// Parse the update date from the product id
java.sql.Date updateDate = new java.sql.Date(productId.getUpdateTime()
.getTime());
// Parse the expiration date from the notification
java.sql.Date expirationDate = new java.sql.Date(notification
.getExpirationDate().getTime());
// Read the URL value from the notification
String trackerUrl = notification.getTrackerURL() != null
? notification.getTrackerURL().toString()
: "";
// Set the values we parsed above
_dml_addNotification.setString(1, productId.getSource());
_dml_addNotification.setString(2, productId.getType());
_dml_addNotification.setString(3, productId.getCode());
_dml_addNotification.setDate(4, updateDate);
_dml_addNotification.setDate(5, expirationDate);
_dml_addNotification.setString(6, trackerUrl);
// If this is a URL notification, set the product URL value as well
if (notification instanceof URLNotification) {
String productUrl = ((URLNotification) notification)
.getProductURL().toString();
_dml_addNotification.setString(7, productUrl);
} else {
_dml_addNotification.setString(7, "");
}
// already verified above
Connection conn = getConnection();
try {
// Begin a transaction
conn.setAutoCommit(false);
// Execute the query
_dml_addNotification.executeUpdate();
// Commit the changes
conn.setAutoCommit(true);
} catch (SQLException sqx) {
// Undo any changes that may be in an unknown state. Ignore
// exceptions that occur in this call since we're already throwing
// an exception
try {
conn.rollback();
} catch (SQLException ex) {
}
// Re-throw this exception
throw sqx;
} finally {
conn.setAutoCommit(true);
}
}
/**
* Remove a notification from the index.
*
* All matching notifications should be removed from the index.
*
* @param notification
* the notification to remove.
* @throws Exception
* if an error occurs while removing the notification.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized void removeNotification(Notification notification)
throws Exception {
// verify connection
this.verifyConnection();
// Read the product id from the notification
ProductId productId = notification.getProductId();
// Parse the update date from the product id
java.sql.Date updateDate = new java.sql.Date(productId.getUpdateTime()
.getTime());
// Parse the expiration date from the notification
java.sql.Date expirationDate = new java.sql.Date(notification
.getExpirationDate().getTime());
// Read the URL value from the notification
String trackerUrl = notification.getTrackerURL() != null
? notification.getTrackerURL().toString()
: "";
// Set the values we parsed above
_dml_removeNotification.setString(1, productId.getSource());
_dml_removeNotification.setString(2, productId.getType());
_dml_removeNotification.setString(3, productId.getCode());
_dml_removeNotification.setDate(4, updateDate);
_dml_removeNotification.setDate(5, expirationDate);
_dml_removeNotification.setString(6, trackerUrl);
// If this is a URL notification, set the product URL value as well
if (notification instanceof URLNotification) {
String productUrl = ((URLNotification) notification)
.getProductURL().toString();
_dml_removeNotification.setString(7, productUrl);
} else {
// _dml_removeNotification.setNull(7, java.sql.Types.VARCHAR);
_dml_removeNotification.setString(7, "");
}
// already verified above
Connection conn = getConnection();
try {
// Begin a transaction
conn.setAutoCommit(false);
// Execute the query
_dml_removeNotification.executeUpdate();
// Commit the changes
conn.setAutoCommit(true);
} catch (SQLException sqx) {
// Undo any changes that may be in an unknown state. Ignore
// exceptions that occur in this call since we're already throwing
// an exception
try {
conn.rollback();
} catch (SQLException ex) {
}
// Re-throw this exception
throw sqx;
} finally {
conn.setAutoCommit(true);
}
}
/**
* Remove notifications from the index.
*
* All matching notifications should be removed from the index.
*
* @param notifications
* the notifications to remove.
* @throws Exception
* if an error occurs while removing the notifications.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized void removeNotifications(List<Notification> notifications)
throws Exception {
for (Notification notification : notifications) {
this.removeNotification(notification);
}
}
/**
* Search the index for notifications matching id.
*
* If more than one notification matches, all should be returned.
*
* @param id
* the ProductId to find.
* @return a list of matching notifications.
* @throws Exception
* if an error occurs while searching the index.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized List<Notification> findNotifications(ProductId id)
throws Exception {
// verify connection
this.verifyConnection();
String source = id.getSource();
String type = id.getType();
String code = id.getCode();
java.sql.Date update = new java.sql.Date(id.getUpdateTime().getTime());
_query_findNotificationsById.setString(1, source);
_query_findNotificationsById.setString(2, type);
_query_findNotificationsById.setString(3, code);
_query_findNotificationsById.setDate(4, update);
return getNotifications(_query_findNotificationsById);
}
/**
* Search the index for notifications matching the sources, types, and
* codes.
*
* Only one notification for each unique ProductId
* (source+type+code+updateTime) should be returned. If sources, types,
* and/or codes are null, that parameter should be considered a wildcard. If
* sources, types, and codes are all null, a notification for each unique
* ProductId in the index should be returned.
*
* @param source
* sources to include, or all if null.
* @param type
* types to include, or all if null.
* @param code
* codes to include, or all if null.
* @return a list of matching notifications.
* @throws Exception
* if an error occurs while searching the index.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized List<Notification> findNotifications(String source,
String type, String code) throws Exception {
// verify connection
this.verifyConnection();
source = (source == null) ? "%" : source.toUpperCase();
type = (type == null) ? "%" : type.toUpperCase();
code = (code == null) ? "%" : code.toUpperCase();
_query_findNotificationsByData.setString(1, source);
_query_findNotificationsByData.setString(2, type);
_query_findNotificationsByData.setString(3, code);
return getNotifications(_query_findNotificationsByData);
}
/**
* Search the index for notifications matching the sources, types, and
* codes.
*
* Only one notification for each unique ProductId
* (source+type+code+updateTime) should be returned. If sources, types,
* and/or codes are null, that parameter should be considered a wildcard. If
* sources, types, and codes are all null, a notification for each unique
* ProductId in the index should be returned.
*
* This implementation require synchronization to prevent SQLExceptions
* caused by concurrent access. SQLite locks the database whenever there is
* an open ResultSet resource. So even read queries can end up causing SQL
* concurrent access problems.
*
* @param sources
* sources to include, or all if null.
* @param types
* types to include, or all if null.
* @param codes
* codes to include, or all if null.
* @return a list of matching notifications.
* @throws Exception
* if an error occurs while searching the index.
*/
public synchronized List<Notification> findNotifications(
List<String> sources, List<String> types, List<String> codes)
throws Exception {
// verify connection
this.verifyConnection();
List<Notification> n = null;
Connection conn = getConnection();
try {
// begin a transaction
conn.setAutoCommit(false);
// Create a temporary lookup table
_dml_createTmpTable.executeUpdate();
// Populate the temporary lookup table with our given lists
if (sources != null) {
// Not null, insert values
Iterator<String> iter = sources.iterator();
while (iter.hasNext()) {
_dml_addTmpSource.setString(1, iter.next());
_dml_addTmpSource.addBatch();
}
_dml_addTmpSource.executeBatch();
}
if (types != null) {
// Not null, insert values
Iterator<String> iter = types.iterator();
while (iter.hasNext()) {
_dml_addTmpType.setString(1, iter.next());
_dml_addTmpType.addBatch();
}
_dml_addTmpType.executeBatch();
}
if (codes != null) {
// Not null, insert values
Iterator<String> iter = codes.iterator();
while (iter.hasNext()) {
_dml_addTmpCode.setString(1, iter.next());
_dml_addTmpCode.addBatch();
}
_dml_addTmpCode.executeBatch();
}
// is this a problem? reading with uncommitted writes?
PreparedStatement ps = getCorrectStatement(sources, types, codes);
n = getNotifications(ps);
} finally {
conn.rollback();
// todo: this looks funky, but it's re-enabling autoCommit, which is
// needed for selects to not block other transactions
conn.setAutoCommit(true);
}
return n;
}
/**
* Search the index for expired notifications.
*
* All expired notifications, even if duplicate, should be returned.
*
* @return a list of expired notifications.
* @throws Exception
* if an error occurs while searching the index.
* @see gov.usgs.earthquake.distribution.NotificationIndex
*/
public synchronized List<Notification> findExpiredNotifications()
throws Exception {
// verify connection
this.verifyConnection();
// Create a new calendar object set to current date/time
java.sql.Date curDate = new java.sql.Date((new Date()).getTime());
// Bind the expiration date parameter and run the query
_query_findExpiredNotifications.setDate(1, curDate);
return getNotifications(_query_findExpiredNotifications);
}
/**
* Executes a prepared statement and parses the result set into a list of
* notifications. The prepared statement can have any set of criteria and
* all required parameters should be bound before calling this method. The
* result set of the prepared statement must include at least: -
* PRODUCT_SOURCE_COLUMN<br>
* - PRODUCT_TYPE_COLUMN<br>
* - PRODUCT_CODE_COLUMN<br>
* - PRODUCT_UPDATE_COLUMN<br>
* - EXPIRATION_DATE_COLUMN<br>
* - TRACKER_URL_COLUMN<br>
* - PRODUCT_URL_COLUMN<br>
*
* @param ps
* The prepared statement to execute.
* @return A list of notifications returned by executing the statement.
* @throws Exception
* If a <code>SQLException</code> occurs.
*/
protected synchronized List<Notification> getNotifications(PreparedStatement ps)
throws Exception {
List<Notification> n = new ArrayList<Notification>();
ResultSet rs = null;
try {
rs = ps.executeQuery();
while (rs.next()) {
n.add(parseNotification(rs.getString(PRODUCT_SOURCE_COLUMN),
rs.getString(PRODUCT_TYPE_COLUMN),
rs.getString(PRODUCT_CODE_COLUMN),
rs.getDate(PRODUCT_UPDATE_COLUMN),
rs.getDate(EXPIRATION_DATE_COLUMN),
rs.getString(TRACKER_URL_COLUMN),
rs.getString(PRODUCT_URL_COLUMN)));
}
} finally {
try {
rs.close();
} catch (Exception e) {
//ignore
}
}
return n;
}
/**
* Creates and returns a <code>Notification</code> based on the provided
* data. If the <code>download</code> string references a valid URL, then a
* <code>URLNotification</code> is created, otherwise a
* <code>DefaultNotification</code> is created.
*
* @param source
* The product source string.
* @param type
* The product type string.
* @param code
* The product code string.
* @param update
* The latest update date/time for the product.
* @param expires
* The date/time when this notification expires.
* @param tracker
* A reference to a URL where information about this product is
* posted.
* @param download
* A reference to a URL where one can download this product, or
* <code>null</code> if this is not a
* <code>URLNotification</code>.
*
* @return The generated notification, or <code>null</code> if one could not
* be created (but an exception did not occur).
*
* @throws Exception
* If the <code>tracker</code> string cannot be successfully
* parsed into a valid URL.
*/
protected Notification parseNotification(String source, String type,
String code, java.sql.Date update, java.sql.Date expires,
String tracker, String download) throws Exception {
Notification n = null;
ProductId productId = new ProductId(source, type, code, update);
URL trackerURL = !"".equals(tracker) ? new URL(tracker) : null;
try {
n = new URLNotification(productId, expires, trackerURL, new URL(download));
} catch (MalformedURLException mux) {
n = new DefaultNotification(productId, expires, trackerURL);
}
return n;
}
/**
* @param sources List string of sources
* @param types List string of types
* @param codes List string of codes
* @return prepared query based on what is/is not null
* @throws Exception if error occurs
*/
protected PreparedStatement getCorrectStatement(List<String> sources,
List<String> types, List<String> codes) throws Exception {
if (sources != null && types != null && codes != null) {
return _query_searchBySourceTypeCode;
} else if (sources != null && types != null && codes == null) {
return _query_searchBySourceType;
} else if (sources != null && types == null && codes != null) {
return _query_searchBySourceCode;
} else if (sources == null && types != null && codes != null) {
return _query_searchByTypeCode;
} else if (sources != null && types == null && codes == null) {
return _query_searchBySource;
} else if (sources == null && types != null && codes == null) {
return _query_searchByType;
} else if (sources == null && types == null && codes != null) {
return _query_searchByCode;
} else if (sources == null && types == null && codes == null) {
return _query_getAllNotifications;
}
return null;
}
}