package uk.ac.cam.cstibhotel.otcanalyser.database;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import uk.ac.cam.cstibhotel.otcanalyser.communicationlayer.Search;
import uk.ac.cam.cstibhotel.otcanalyser.communicationlayer.SearchResult;
import uk.ac.cam.cstibhotel.otcanalyser.trade.Action;
import uk.ac.cam.cstibhotel.otcanalyser.trade.AssetClass;
import uk.ac.cam.cstibhotel.otcanalyser.trade.Trade;
import uk.ac.cam.cstibhotel.otcanalyser.trade.TradeType;
/**
* A database class for storing OTC trades
*
* @author Wai-Wai Ng
*/
public class Database {
private static Database db;
private static Connection connection;
private static final int TableAlreadyExistsError = -21; // as defined by HSQLDB Driver
private static final int ObjectNameAlreadyExists = -5504; // as thrown by trying to create table twice
private static final int RowWithUniqueFieldAlreadyExistsError = -104;
/**
* Returns a string representing the filepath to where the database should be stored
* @return the file path to the database
*/
private static String getDatabasePath() {
// originally stored in the appropriate directory for OSes, but it
// transpired that permissions issues were rather more likely than
// expected, so this was replaced with a local filepath
return "database.db";
}
/**
* Returns the singleton database object
* @return the database
*/
public static Database getDB() {
if (db==null) {
try {
db = new Database();
} catch (SQLException ex) {
System.err.println("There was a fatal database error.");
System.err.println(ex.getMessage());
System.exit(1);
} catch (ClassNotFoundException e) {
System.err.println("Error: Could not locate database driver");
System.exit(2);
}
}
return db;
}
/**
* Creates the database
* @throws SQLException
* @throws ClassNotFoundException
*/
private Database() throws SQLException, ClassNotFoundException {
Class.forName("org.hsqldb.jdbcDriver");
connection = DriverManager.getConnection("jdbc:hsqldb:file:"+getDatabasePath());
connection.setAutoCommit(false);
connection.createStatement().execute("SET WRITE_DELAY FALSE"); // always update data on disk
createDataTable();
createInfoTable();
createSavedSearchTable();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
connection.createStatement().execute("SHUTDOWN");
} catch (SQLException ex) {
System.err.println("Database shutdown failed");
}
}
});
}
/**
* Creates the table for trade data
* @throws SQLException
*/
private void createDataTable() throws SQLException {
StringBuilder dataTableCreator = new StringBuilder("CREATE TABLE data (");
HashMap<String, SQLField> DBNameDBType = TradeFieldMapping.getMapping(new Trade());
Iterator<Entry<String, SQLField>> i = DBNameDBType.entrySet().iterator();
while (i.hasNext()) {
Entry<String, SQLField> mapEntry = i.next();
if(mapEntry != null && mapEntry.getValue() != null){
dataTableCreator.append(mapEntry.getKey()).append(" ").append(mapEntry.getValue().getType()).append(", ");
} else {
if(mapEntry == null){ // TODO
System.out.println("a");
} else {
System.out.println("b");
}
// dataTableCreator.app
}
}
dataTableCreator.append(" UNIQUE (id));");
try {
connection.createStatement().execute(dataTableCreator.toString());
connection.createStatement().executeQuery("CREATE INDEX ua1 ON data (underlyingAsset1)");
connection.createStatement().executeQuery("CREATE INDEX eTcurr ON data (executionTime, notionalCurrency1)");
connection.createStatement().executeQuery("CREATE INDEX rna1 ON data (roundedNotionalAmount1)");
} catch (SQLException e) {
if(e.getErrorCode() != TableAlreadyExistsError && e.getErrorCode() != ObjectNameAlreadyExists){
System.err.println(e.getErrorCode());
throw e;
}
}
}
/**
* Creates the table that stores internal metadata
* @throws SQLException
*/
private void createInfoTable() throws SQLException {
String infoTableCreator = "CREATE TABLE info ( key VARCHAR(255), vvalue VARCHAR(255) )";
try {
connection.createStatement().execute(infoTableCreator);
connection.createStatement().execute("INSERT INTO info (key, vvalue) VALUES ('last_update', '0')");
commit();
} catch (SQLException e) {
if(e.getErrorCode() != TableAlreadyExistsError && e.getErrorCode() != ObjectNameAlreadyExists){
throw e;
}
}
}
/**
* Creates the table that stores saved searches
* @throws SQLException
*/
private void createSavedSearchTable() throws SQLException {
String savedSearchTableCreator = "CREATE TABLE savedsearches ("
+ " id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,"
+ " searchName VARCHAR(255),"
+ " tradeType SMALLINT, "
+ " assetClass SMALLINT, "
+ " asset VARCHAR(255), "
+ " minPrice BIGINT, "
+ " maxPrice BIGINT, "
+ " currency VARCHAR(20), "
+ " startTime TIMESTAMP, "
+ " endTime TIMESTAMP, "
+ " upi VARCHAR(255),"
+ " UNIQUE (searchName))";
try{
connection.createStatement().execute(savedSearchTableCreator);
} catch (SQLException e){
if(e.getErrorCode() != TableAlreadyExistsError && e.getErrorCode() != ObjectNameAlreadyExists){
throw e;
}
}
}
/**
* Attempts to commit the current transaction to the database
* @return true if the current transaction was successfully committed
*/
private boolean commit() {
try {
connection.createStatement().execute("COMMIT;");
return true;
} catch (SQLException ex) {
System.err.println(ex.getMessage());
return false;
}
}
/**
* Adds trades to the database
*
* @param trades a list of trades to be added to the database
* @return true if the database was successfully updated
*/
public boolean addTrade(List<Trade> trades) {
boolean success = true;
Collections.sort(trades);
for (Trade trade : trades) {
HashMap<String, SQLField> DBNameValue = TradeFieldMapping.getMapping(trade);
Iterator<Entry<String, SQLField>> iterator = DBNameValue.entrySet().iterator();
String executeString;
if (trade.getAction().equals(Action.CANCEL)) {
boolean v = deleteTrade(trade.getDisseminationID());
success &= v; // do not combine this with the above as Java will short-circuit boolean evaluation
break;
} else if (trade.getAction().equals(Action.CORRECT)) {
executeString = buildUpdateString(iterator, trade.getDisseminationID());
} else { // equals(Action.NEW)
executeString = buildInsertString(iterator);
}
try {
PreparedStatement p = connection.prepareStatement(executeString);
iterator = DBNameValue.entrySet().iterator();
while (iterator.hasNext()) {
iterator.next().getValue().addToPreparedStatement(p);
}
p.execute();
} catch (SQLException e) {
if(e.getErrorCode() == RowWithUniqueFieldAlreadyExistsError){
// ignore, we've just tried to duplicate a row
} else {
System.err.println(e.getMessage());
System.err.println("Failed to insert/update a row");
success = false;
}
}
updateLastUpdateTime(trade);
}
boolean v = commit();
return success && v;
}
/**
*
* @param iterator An iterator over a DB mapping
* @return An SQL INSERT string
*/
private String buildInsertString(Iterator<Entry<String, SQLField>> iterator) {
StringBuilder a = new StringBuilder("INSERT INTO data (");
StringBuilder b = new StringBuilder(") VALUES (");
int index = 1;
while (iterator.hasNext()) {
Entry<String, SQLField> mapEntry = iterator.next();
a.append(mapEntry.getKey()).append(", ");
b.append("?, ");
mapEntry.getValue().index = index;
index++;
}
a.setLength(a.length()-2);
b.setLength(b.length()-2);
a.append(b).append(")");
return a.toString();
}
/**
*
* @param iterator An iterator over a DB mapping
* @param origId The id of the trade to update
* @return An SQL UPDATE string
*/
private String buildUpdateString(Iterator<Entry<String, SQLField>> iterator, long origId) {
StringBuilder s = new StringBuilder("UPDATE data SET ");
int index = 1;
while (iterator.hasNext()) {
Entry<String, SQLField> mapEntry = iterator.next();
s.append(mapEntry.getKey()).append(" = ?, ");
mapEntry.getValue().index = index;
index++;
}
s.setLength(s.length()-2);
s.append(" WHERE id = ").append(origId);
return s.toString();
}
/**
*
* @param id The id of the trade to delete
* @return Whether the deletion was successful
*/
private boolean deleteTrade(long id) {
String deletionString = "DELETE FROM data WHERE id = "+id;
try {
connection.createStatement().execute(deletionString);
} catch (SQLException ex) {
System.err.println("Error deleting a trade");
return false;
}
return true;
}
/**
* Updates the metadata field that stores the most recent date for which the database has data
* @param trade the trade to use to calculate the last update time
*/
private boolean updateLastUpdateTime(Trade trade) {
java.util.Date thisUpdateTime = trade.getExecutionTimestamp();
java.util.Date lastUpdateTime = getLastUpdateTime();
if (thisUpdateTime==null) {
return true;
}
if (thisUpdateTime.after(lastUpdateTime)) {
try {
PreparedStatement ps = connection.prepareCall("UPDATE info SET vvalue = ? WHERE key = 'last_update'");
ps.setString(1, Long.toString(thisUpdateTime.getTime()));
ps.execute();
} catch (SQLException e) {
System.err.println(e.getMessage());
System.err.println("Failed to update last update time");
}
}
return true;// commit();
}
/**
* Returns the most recent date for which the database has data
* @return The date of the last database update
*/
public java.util.Date getLastUpdateTime() {
try {
Statement s = connection.createStatement();
s.execute("SELECT vvalue FROM info WHERE key = 'last_update'");
String timeString;
ResultSet rs = s.getResultSet();
if (rs.next()) {
timeString = rs.getString(1);
if (Long.parseLong(timeString)==0L) {
Calendar c = Calendar.getInstance();
// First available data is from 28th February 2013
c.set(2013, 1, 27);
return c.getTime();
}
return new Date(Long.parseLong(timeString));
} else {
throw new RuntimeException("no data");
}
} catch (SQLException ex) {
System.err.println(ex.getMessage());
System.err.println("Could not get the last update time");
return new java.util.Date(0);
}
}
/**
* Searches the database
* @param s the search parameters
* @return all data matching the search
*/
public SearchResult search(Search s) {
try {
String query = "SELECT * FROM data WHERE "
+"tradeType = ? AND "
+"assetClass = ? AND ";
if (!(s.getAsset() == null||s.getAsset().equals(""))) {
query += " (underlyingAsset1 LIKE ? OR underlyingAsset2 LIKE ?) AND ";
}
if (!(s.getMinPrice() == s.getMaxPrice())){
query += " roundedNotionalAmount1 >= ? AND "
+" roundedNotionalAmount1 <= ? AND ";
}
if (!(s.getCurrency() == null || s.getCurrency().equals(""))) {
query += " (notionalCurrency1 LIKE ? OR notionalCurrency2 LIKE ? ) AND ";
}
if (!(s.getUPI() == null || s.getUPI().equals(""))){
query += " taxonomy LIKE ? AND ";
}
query += " executionTime >= ? AND "
+" executionTime <= ?";
PreparedStatement ps = connection.prepareStatement(query);
int i = 1;
ps.setShort(i, s.getTradeType().getValue()); i++;
ps.setShort(i, s.getAssetClass().getValue()); i++;
if (!(s.getAsset() == null||s.getAsset().equals(""))) {
ps.setString(i, "%"+s.getAsset()+"%"); i++;
ps.setString(i, "%"+s.getAsset()+"%"); i++;
}
if(!(s.getMinPrice() == s.getMaxPrice())){
ps.setFloat(i, s.getMinPrice()); i++;
ps.setFloat(i, s.getMaxPrice()); i++;
}
if (!(s.getCurrency() == null || s.getCurrency().equals(""))) {
ps.setString(i, "%"+s.getCurrency()+"%"); i++;
ps.setString(i, "%"+s.getCurrency()+"%"); i++;
}
if (!(s.getUPI() == null || s.getUPI().equals(""))){
ps.setString(i, "%" + s.getUPI() + "%"); i++;
}
ps.setTimestamp(i, new Timestamp(s.getStartTime().getTime())); i++;
ps.setTimestamp(i, new Timestamp(s.getEndTime().getTime())); i++;
ResultSet rs = ps.executeQuery();
List<Trade> trades = new LinkedList<>();
while (rs.next()) {
trades.add(TradeFieldMapping.makeObjectFromRecord(rs));
}
System.out.println(trades.size());
return new SearchResult(trades, 0);
} catch (SQLException ex) {
System.out.println(ex.getMessage());
System.err.println("Search failed");
return new SearchResult(new LinkedList<Trade>(), 0.0);
}
}
/**
* Saves a search
* @param s the search to save
* @param searchName the name of this search
* @return true if the search was successfully saved; false otherwise
*/
public boolean saveSearch(Search s, String searchName) {
try{
String searchAlreadyExistsQuery = "SELECT COUNT(1) FROM savedSearches WHERE searchName = ?";
PreparedStatement sae = connection.prepareStatement(searchAlreadyExistsQuery);
sae.setString(1, searchName);
ResultSet saer = sae.executeQuery();
String saveQuery;
if(saer.next()){
if(saer.getInt(1) == 0){
saveQuery = "INSERT INTO savedSearches (searchName, tradeType, assetClass, "
+ "asset, minPrice, maxPrice, currency, startTime, endTime, upi) VALUES "
+ "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
} else {
saveQuery = "UPDATE savedSearches SET searchName = ?, tradeType = ?, "
+ "assetClass = ?, asset = ?, minPrice = ?, maxPrice = ?, "
+ "currency = ?, startTime = ?, endTime = ?, upi = ? "
+ "WHERE searchName = ?";
}
} else {
return false;
}
/* String saveQuery = "MERGE INTO savedSearches USING (VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)) "
+ "AS I(a, b, c, d, e, f, g, h, i, j) ON savedSearches.searchName = I.a "
+ "WHEN MATCHED THEN UPDATE SET savedSearches.tradeType = I.b, "
+ "savedSearches.assetClass = I.c, savedSearches.asset = I.d, "
+ "savedSearches.minPrice = I.e, savedSearches.maxPrice = I.f, "
+ "savedSearches.currency = I.g, savedSearches.startTime = I.h, "
+ "savedSearches.endTime = I.i, savedSearches.upi = I.j "
+ "WHEN NOT MATCHED THEN INSERT (searchName, tradeType, assetClass, "
+ "asset, minPrice, maxPrice, currency, startTime, endTime, upi) "
+ "VALUES (I.a, I.b, I.c, I.d, I.e, I.f, I.g, I.h, I.i, I.j)";
// So it turns out HSQLDB doesn't support parameterized MERGE statements
*/
PreparedStatement ps = connection.prepareStatement(saveQuery);
ps.setString(1, searchName);
ps.setShort(2, s.getTradeType().getValue());
ps.setShort(3, s.getAssetClass().getValue());
ps.setString(4, s.getAsset());
ps.setLong(5, s.getMinPrice());
ps.setLong(6, s.getMaxPrice());
ps.setString(7, s.getCurrency());
ps.setTimestamp(8, new Timestamp(s.getStartTime().getTime()));
ps.setTimestamp(9, new Timestamp(s.getEndTime().getTime()));
ps.setString(10, s.getUPI());
if(saer.getInt(1) == 1){ // yes this is silly
ps.setString(11, searchName);
}
ps.execute();
return true;
} catch (SQLException e){
System.err.println(e.getMessage());
return false;
}
}
/**
* Returns a saved search by string for the search name
* @param name the name of the search to lookup;
* @return the Search that matches the requested string, or null if no search exists/a db failure occurred
*/
public static Search getSavedSearch(String name) {
String query = "SELECT * FROM savedSearches WHERE searchName = ?";
try{
if(connection == null){
getDB();
}
PreparedStatement ps = connection.prepareStatement(query);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
if(rs.next()){
Search s = new Search();
s.setTradeType(TradeType.lookup(rs.getShort("tradeType")));
s.setAssetClass(AssetClass.lookup(rs.getShort("assetClass")));
s.setAsset(rs.getString("asset"));
s.setMinPrice(rs.getInt("minPrice"));
s.setMaxPrice(rs.getInt("maxPrice"));
s.setCurrency(rs.getString("currency"));
s.setStartTime(new Timestamp(rs.getTimestamp("startTime").getTime()));
s.setEndTime(new Timestamp(rs.getTimestamp("endTime").getTime()));
s.setUPI(rs.getString("upi"));
return s;
}
} catch (SQLException e){
System.err.println(e.getMessage());
return null;
}
return null;
}
/**
* Returns the five searches most recently saved
* @return The 5 most recently saved previously saved searches
*/
public static Map<String, Search> getSavedSearches() {
Map<String, Search> savedSearches = new LinkedHashMap<>();
if(connection == null){
getDB();
}
String query = "SELECT * FROM savedSearches ORDER BY id DESC LIMIT 5";
try {
ResultSet rs = connection.createStatement().executeQuery(query);
while(rs.next()){
Search s = new Search();
s.setTradeType(TradeType.lookup(rs.getShort("tradeType")));
s.setAssetClass(AssetClass.lookup(rs.getShort("assetClass")));
s.setAsset(rs.getString("asset"));
s.setMinPrice(rs.getInt("minPrice"));
s.setMaxPrice(rs.getInt("maxPrice"));
s.setCurrency(rs.getString("currency"));
s.setStartTime(new Timestamp(rs.getTimestamp("startTime").getTime()));
s.setEndTime(new Timestamp(rs.getTimestamp("endTime").getTime()));
s.setUPI(rs.getString("upi"));
savedSearches.put(rs.getString("searchName"), s);
}
} catch (SQLException ex) {
Logger.getLogger(Database.class.getName()).log(Level.SEVERE, null, ex);
}
return savedSearches;
}
/**
* Provides a DB Connection to allow raw SQL where needed
*
* This method is provided for cases where direct exposure to the DB API is
* necessary. Its use breaks the information hiding principle and its use is
* therefore discouraged.
*
* @return The database Connection
*/
public Connection getConnection() {
return connection;
}
}