package ca.uwaterloo.fydp.db;
import ca.uwaterloo.fydp.ossp.OSSPStimulus;
import ca.uwaterloo.fydp.xcde.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Timestamp;
/**
* The backend for the source replay client. Implements the XCDEDocClient API to simplify
* integration, but does not accept changes, inbound changes cause a runtime exception.
* @author Andrew Craik
* @version 1.0
*/
public class clientJDBCPipe implements XCDEDocClient, Runnable
{
private Connection conn;
private String mysqlHostname;
private int mysqlPort;
private String mysqlDBUser;
private String mysqlDBPassword;
private String path;
private XCDEDocClientCallbacks docClient;
private Timestamp start, end;
private volatile Thread resultThread;
private long separationTime;
private boolean threadSuspended;
//the default values used for the MySQL connection
private static final String DEFAULT_MYSQL_SERVER = "localhost";
private static final int DEFAULT_MYSQL_SERVER_PORT = 3306;
private static final String DEFAULT_DB_USERNAME = "serverDB";
private static final String DEFAULT_DB_PASSWORD = "db1user";
/**
* Create a client to run queries via JDBC on the document history database.
* This class is designed as the backend for the replay viewer. It does NOT support
* receiving changes, only sending the changes out to clients.
* @param hostname the fully qualified DNS name of the host running the MySQL server
* @param port port to use to connect to the MySQL server on hostname
* @param dbUser the username to supply to the MySQL server at connection time
* @param dbPassword the password to supply to the MySQL server at connection time
*/
public clientJDBCPipe(String hostname, int port, String dbUser, String dbPassword)
{
// we have to load the com.mysql.jdbc.Driver class so that when we go to
// make an SQL
// connection we are able to find the specific driver that we need
try
{
Class.forName("com.mysql.jdbc.Driver");
}
catch (ClassNotFoundException e)
{
throw new RuntimeException("Could not locate the com.mysql.jdbc.Driver class!", e);
}
//initialize the values, use defaults if no value given
mysqlHostname = (hostname.equals("")) ? DEFAULT_MYSQL_SERVER : hostname;
mysqlPort = (port == 0) ? DEFAULT_MYSQL_SERVER_PORT : port;
mysqlDBUser = (dbUser.equals("")) ? DEFAULT_DB_USERNAME : dbUser;
mysqlDBPassword = (dbPassword.equals("")) ? DEFAULT_DB_PASSWORD : dbPassword;
separationTime = 500;
threadSuspended = false;
}
/**
* Create a client to run queries via JDBC on the document history database.
* This class is designed as the backend for the replay viewer. It does NOT support
* receiving changes, only sending the changes out to clients. The no args constructor
* creates sets all connection parameters to hardcoded defaults.
*
* It is recommended that you consider using the 4 arg constructor instead.
*/
public clientJDBCPipe()
{
// we have to load the com.mysql.jdbc.Driver class so that when we go to
// make an SQL
// connection we are able to find the specific driver that we need
try
{
Class.forName("com.mysql.jdbc.Driver");
}
catch (ClassNotFoundException e)
{
throw new RuntimeException("Could not locate the com.mysql.jdbc.Driver class!", e);
}
//initialize the default values
mysqlHostname = DEFAULT_MYSQL_SERVER;
mysqlPort = DEFAULT_MYSQL_SERVER_PORT;
mysqlDBUser = DEFAULT_DB_USERNAME;
mysqlDBPassword = DEFAULT_DB_PASSWORD;
separationTime = 500;
threadSuspended = false;
}
/**
* Throws a runtime exception since this XCDEDocClient does not support inbound changes.
*/
public void makeChange(OSSPStimulus ch)
{
throw new RuntimeException("Replay client JDBC pipe cannot accept changes, something isn't written right!");
}
/**
* Attempts to connect to the MySQL server specified by the private fields of the calss.
* Fields can be set with the 4 arg constructor or the setters provided in this class.<br>
* <b>Note:</b> Once you have connected you must disconnect when finished to free the MySQL
* connection.
* @return true if no exceptions occured setting up the connection
*/
public boolean connect()
{
//check if we are connected, if so return true
if (conn != null)
return true;
// now we have the MySQL JDBC dirver loaded we need to connect up using
// either the user
// supplied or default values for the different parameters
try
{
conn = DriverManager.getConnection("jdbc:mysql://" + mysqlHostname
+ ":" + mysqlPort + "/xcde", mysqlDBUser, mysqlDBPassword);
conn.setAutoCommit(true);
}
catch (SQLException e)
{
System.err.println(e);
return false;
}
return true;
}
/**
* Close the open connection to the MySQL server
*/
public void disconnect()
{
//null the thread so it terminates when it wakes up or next iterates
resultThread = null;
if (conn == null)
return;
//shutdown the JDBC connection
try
{
conn.close();
}
catch (SQLException e)
{
System.err.println(e);
return;
}
conn = null;
}
/**
* Called to trigger the download of the initial file contents and start the thread
* to send the changes sequentially. Used to load and run the client replay.
*/
public void beginDownloadState()
{
if (!connect())
throw new RuntimeException("Error connecting with MySQL server, download aborted!");
//now we need to retrieve the file contents at the time closest to our start time
StringBuffer fileContents = new StringBuffer("");
Timestamp fileContentStart = null;
String filename = path.substring(path.lastIndexOf("/")+1);
String filepath = path.substring(0,path.lastIndexOf("/")+1);
try
{
//first download the newst file contents for the file from the FileContents table
Statement contentQuery = conn.createStatement();
String query = "SELECT contents, addDate, fileID FROM FileContents" +
" WHERE fileID = (SELECT fileID FROM Files INNER JOIN Directories ON Files.DirLoc = Directories.dirID" +
" WHERE Files.FileName = '" + filename + "' AND Directories.FullName = '" + filepath + "'" +
" AND (Directories.DropDate < '" + start +"' OR Directories.DropDate IS NULL)" +
" AND (Files.DropDate < '" + start + "' OR Files.DropDate IS NULL))" +
" AND addDate < '" + start + "' ORDER BY addDate";
System.out.println(query);
ResultSet rs = contentQuery.executeQuery(query);
rs.last();
System.out.println(rs.getRow());
if (rs.getRow() == 0)
{
fileContentStart = start;
}
else
{
fileContentStart = rs.getTimestamp("addDate");
//append the file contents to our buffer
fileContents.append(rs.getString("contents"));
}
rs.close();
//now query the change table for changes between the contents and the start time for the replay
//and apply those changes to the initial state before sending
rs = contentQuery.executeQuery(buildChangeSetQuery(fileContentStart, start, path));
rs.last();
if (rs.getRow() != 0)
{
rs.first();
for (rs.first(); !rs.isAfterLast(); rs.next())
{
if (rs.getString("ChangeType").equals("I"))
{
fileContents.insert(rs.getInt("ChangePos"), rs.getString("ChangeText"));
}
else if (rs.getString("ChangeType").equals("D"))
{
fileContents.replace(rs.getInt("ChangePos"), rs.getInt("ChangePos") + Integer.parseInt(rs.getString("ChangeText")), "");
}
else
{
throw new RuntimeException("Unknown change type in preprocessing the file state");
}
}
}
}
catch (SQLException e)
{
throw new RuntimeException("SQLException getting the initial file contents for " + path + " ", e);
}
//send the initial document contents to the client
docClient.receiveState(this, new XCDEDocument(fileContents.toString()));
//create a thread to send the changes
resultThread = new Thread(this, "result thread");
resultThread.start();
}
/**
* Calling this method will cause the stream of incoming changes to pause. No changes
* will be sent to the client again until resumePlayback is called.
*/
public synchronized void pausePlayback()
{
threadSuspended = true;
}
/**
* Calling this method will cause the stream of incoming changes to resume if the
* stream was previously paused by pausePlayback. If the playback is not paused,
* no change in operation will occur.
*/
public synchronized void resumePlayback()
{
if (!threadSuspended)
return;
threadSuspended = false;
notify();
}
/**
* Stops the replay currently in progress permanently. Once this is called, the
* position in the replay history if forgotten and beginDownloadState will be need
* to be called to play the file again.
*/
public void stopPlayback()
{
resultThread = null;
if (threadSuspended)
notify();
}
/**
* Resets the server configuration after disconnecting from any current
* session.
* @param host Hostname of the db server to be connected to
* @param port The port to connect to the db server on
* @param user The user name to be used when connecting
* @param pw The password to be used when connecting
*/
public void setDBConfig(String host, int port, String user, String pw)
{
disconnect();
mysqlHostname = host;
mysqlPort = port;
mysqlDBUser = user;
mysqlDBPassword = pw;
}
/**
* Called to set the object to which change messages will be sent
*/
public void setCallback(XCDEDocClientCallbacks cb)
{
resultThread = null;
docClient = cb;
}
/**
* Get the full path and file that changes are being sent.
*/
public String getPath()
{
return path;
}
/**
* Set the fully qualified name of the file to be sent
* @param p fully qualified name of the file
*/
public void setPath(String p)
{
resultThread = null;
path = p;
}
/**
* Return the start time for the change window that will be sent to the client
* @return The timestamp representing the oldest changes that will be sent
*/
public Timestamp getStartTime()
{
return start;
}
/**
* Return the end time for the change window that will be sent to the client
* @return The timestamp representing the cutoff for newest changes that will be sent
*/
public Timestamp getEndTime()
{
return end;
}
/**
* Sets the replay start time to s. Calling this method causes any
* currently running replay to be terminated.
* @param s Timestamp representing the start of the replay time window
*/
public void setStartTime(Timestamp s)
{
resultThread = null;
start = s;
}
/**
* Sets the replay end time to e. This method causes any currently running
* replay to be terminated.
* @param e
*/
public void setEndTime(Timestamp e)
{
resultThread = null;
end = e;
}
/**
* Set the time interval to elapse between changes.
* @param t The time in milliseconds to wait between each change send
*/
public void setTimeInterval(long t)
{
separationTime = t;
}
/**
* Get the time interval that is to elapse between the dispatch of changes to the client
* @return Time in milliseconds between change notifications from the server to the client
*/
public long getTimeInterval()
{
return separationTime;
}
/**
* Build a query to get a set of change events between a start and end date
* @param s The start time for the change set
* @param e The end time for the change set
* @param p The fully qualified name of the file to generate the change set for
* @return The query to run to generate the set as a string
*/
private String buildChangeSetQuery(Timestamp s, Timestamp e, String p)
{
String fileInnerQuery =
"(SELECT fileId FROM Files WHERE FileName = '" + p.substring(p.lastIndexOf("/")+1) +
"' AND dirLoc=(SELECT dirID FROM Directories WHERE FullName = '" + p.substring(0,p.lastIndexOf("/")+1)
+ "' AND AddDate < '" + s + "' AND (DropDate > '" + e + "' OR DropDate IS NULL))" +
" AND AddDate < '" + s + "' AND (DropDate > '" + e + "' OR DropDate IS NULL))";
return "SELECT FileChanges.ChangeType, FileChanges.ChangePos, FileChanges.ChangeText, Users.ShortName FROM FileChanges, Users" +
" WHERE Users.usersID = FileChanges.UserID AND FileChanges.fileID = " + fileInnerQuery +" AND FileChanges.ChangeTime > '" + s + "' AND FileChanges.ChangeTime < '" + e + "'";
}
/**
* The implementation of the Runnable interface that is used to send the changes at regular
* intervals to the client. The thread terminates after the last row is sent to the client
* or when the thread is nolonger held in the resultThread field (this allows us to shutdown
* the thread easily).
*/
public void run()
{
Thread thisThread = Thread.currentThread();
//start sending changes
ResultSet rs;
try
{
Statement st = conn.createStatement();
System.out.println(buildChangeSetQuery(start, end, path));
rs = st.executeQuery(buildChangeSetQuery(start, end, path));
rs.last();
if(rs.getRow() == 0)
{
rs.close();
return;
}
rs.first();
}
catch (SQLException e)
{
throw new RuntimeException("error running client viewer query!", e);
}
try
{
//go to first row of result set and iterate to the last calling the docClient at
//the specified interval
for (rs.first(); !rs.isAfterLast() && resultThread == thisThread; rs.next())
{
//sleep for the required interval
try
{
Thread.sleep(separationTime);
synchronized(this)
{
while (threadSuspended)
wait();
}
}
catch (InterruptedException e)
{
System.err.println(e);
}
//build the change message to send to the client
XCDEDocChange change = null;
if(rs.getString("ChangeType").equals("I"))
{
change = new XCDEDocChangeInsertion(rs.getString("ShortName"), rs.getInt("ChangePos"),rs.getString("ChangeText"));
}
else if (rs.getString("ChangeType").equals("D"))
{
change = new XCDEDocChangeDeletion(rs.getString("ShortName"), rs.getInt("ChangePos"),Integer.parseInt(rs.getString("ChangeText")));
}
else
{
throw new RuntimeException("Uknown change type in result set: " + rs.getString(0));
}
//send the change
docClient.notifyOfChange(this,change);
}
rs.close();
}
catch (SQLException e)
{
throw new RuntimeException("Error running SQL", e);
}
}
public void pace()
{
throw new UnsupportedOperationException("You are not allowed to send changes, don't call pace()!");
}
public void syncExec(Runnable task)
{
throw new UnsupportedOperationException("You are not allowed to send changes, don't call syncExec()!");
}
public void asyncExec(Runnable task)
{
throw new UnsupportedOperationException("You are not allowed to send changes, don't call asyncExec()!");
}
public boolean isConnected()
{
return true;
}
public void open()
{
beginDownloadState();
}
public void close()
{
disconnect();
}
public boolean isOpen()
{
return true;
}
public XCDEDocument getState()
{
// TODO Auto-generated method stub
return null;
}
}