No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DatabaseController.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. package fr.insa.clavardator.client.db;
  2. import fr.insa.clavardator.client.users.CurrentUser;
  3. import fr.insa.clavardator.client.users.PeerUser;
  4. import fr.insa.clavardator.lib.message.FileMessage;
  5. import fr.insa.clavardator.lib.message.Message;
  6. import fr.insa.clavardator.lib.users.User;
  7. import fr.insa.clavardator.lib.users.UserInformation;
  8. import fr.insa.clavardator.lib.util.ErrorCallback;
  9. import fr.insa.clavardator.lib.util.Log;
  10. import org.intellij.lang.annotations.Language;
  11. import org.jetbrains.annotations.Nullable;
  12. import java.io.IOException;
  13. import java.sql.*;
  14. import java.util.ArrayList;
  15. import java.util.Date;
  16. public class DatabaseController {
  17. private Connection connection;
  18. public DatabaseController() {
  19. connect();
  20. }
  21. public DatabaseController(boolean test) {
  22. if (test) {
  23. connectToTestDb();
  24. } else {
  25. connect();
  26. }
  27. }
  28. /**
  29. * Connects to the main database
  30. */
  31. private void connect() {
  32. connectToDatabase("clavardator");
  33. }
  34. /**
  35. * Connects to the test database.
  36. *
  37. * @implNote DO NOT USE OUTSIDE OF TESTS
  38. */
  39. private void connectToTestDb() {
  40. connectToDatabase("clavardator_test");
  41. }
  42. /**
  43. * Connects to the database of the given name
  44. *
  45. * @param dbName The database to connect to
  46. */
  47. private void connectToDatabase(String dbName) {
  48. try {
  49. Class.forName("org.sqlite.JDBC");
  50. connection = DriverManager.getConnection("jdbc:sqlite:" + dbName + ".db");
  51. Log.v(getClass().getSimpleName(), "Opened database '" + dbName + "' successfully");
  52. } catch (ClassNotFoundException | SQLException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. /**
  57. * Closes the connection to the current database
  58. */
  59. public void close() {
  60. try {
  61. connection.close();
  62. } catch (SQLException e) {
  63. e.printStackTrace();
  64. }
  65. }
  66. /**
  67. * Creates the table used to store messages
  68. *
  69. * @param callback The function to call on success
  70. * @param errorCallback The function to call on error
  71. */
  72. private void createMessageTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  73. @Language("SQL") String sql = "CREATE TABLE IF NOT EXISTS message " +
  74. "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
  75. " timestamp DATETIME NOT NULL, " +
  76. " sender TEXT NOT NULL, " +
  77. " recipient TEXT NOT NULL, " +
  78. " text TEXT, " +
  79. " file_path TEXT)";
  80. Log.v(getClass().getSimpleName(), "Creating table message...");
  81. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  82. executor.start();
  83. }
  84. /**
  85. * Creates the table used to store users
  86. *
  87. * @param callback The function to call on success
  88. * @param errorCallback The function to call on error
  89. */
  90. private void createUserTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  91. @Language("SQL") String sql = "CREATE TABLE IF NOT EXISTS user " +
  92. "(id TEXT PRIMARY KEY NOT NULL," +
  93. " username TINYTEXT NULLABLE )";
  94. Log.v(getClass().getSimpleName(), "Creating table user...");
  95. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  96. executor.start();
  97. }
  98. /**
  99. * Creates the table used to store users
  100. *
  101. * @param callback The function to call on success
  102. * @param errorCallback The function to call on error
  103. */
  104. private void createCurrentUserTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  105. @Language("SQL") String sql = "CREATE TABLE IF NOT EXISTS current_user " +
  106. "(id TEXT PRIMARY KEY NOT NULL," +
  107. " username TINYTEXT NULLABLE )";
  108. Log.v(getClass().getSimpleName(), "Creating table current user...");
  109. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  110. executor.start();
  111. }
  112. /**
  113. * Creates all needed tables if non-existent
  114. *
  115. * @param callback The function to call on success
  116. * @param errorCallback The function to call on error
  117. */
  118. public void initTables(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  119. createMessageTable(() -> createCurrentUserTable(() -> createUserTable(callback, errorCallback), errorCallback), errorCallback);
  120. }
  121. /**
  122. * Destroys the message table
  123. *
  124. * @param callback The function to call on success
  125. * @param errorCallback The function to call on error
  126. */
  127. private void dropMessageTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  128. @Language("SQL") String sql = "DROP TABLE IF EXISTS message";
  129. Log.v(getClass().getSimpleName(), "Dropping table message...");
  130. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  131. executor.start();
  132. }
  133. /**
  134. * Destroys the user table
  135. *
  136. * @param callback The function to call on success
  137. * @param errorCallback The function to call on error
  138. */
  139. private void dropUserTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  140. @Language("SQL") String sql = "DROP TABLE IF EXISTS user";
  141. Log.v(getClass().getSimpleName(), "Dropping table user...");
  142. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  143. executor.start();
  144. }
  145. private void dropCurrentUserTable(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  146. @Language("SQL") String sql = "DROP TABLE IF EXISTS current_user";
  147. Log.v(getClass().getSimpleName(), "Dropping table current_user...");
  148. UpdateExecutor executor = new UpdateExecutor(sql, callback, errorCallback);
  149. executor.start();
  150. }
  151. /**
  152. * Destroys all tables
  153. *
  154. * @param callback The function to call on success
  155. * @param errorCallback The function to call on error
  156. */
  157. private void dropTables(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  158. dropMessageTable(() -> dropUserTable(() -> dropCurrentUserTable(callback, errorCallback), errorCallback), errorCallback);
  159. }
  160. /**
  161. * Destroys and recreates all tables
  162. *
  163. * @param callback The function to call on success
  164. * @param errorCallback The function to call on error
  165. */
  166. public void resetTables(@Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  167. dropTables(() -> initTables(callback, errorCallback), errorCallback);
  168. }
  169. /**
  170. * Fetches the list of users for which we already have a chat history
  171. *
  172. * @param callback Function called when the request is done
  173. * @param errorCallback The function to call on error
  174. */
  175. public void getAllUsers(UsersCallback callback, ErrorCallback errorCallback) {
  176. @Language("SQL") String sql = "SELECT * FROM user";
  177. Log.v(getClass().getSimpleName(), "Fetching users from db... ");
  178. QueryExecutor executor = new QueryExecutor(sql, res -> {
  179. ArrayList<User> userList = new ArrayList<>();
  180. while (res.next()) {
  181. String id = res.getString("id");
  182. String username = res.getString("username");
  183. userList.add(new PeerUser(id, username));
  184. }
  185. Log.v(getClass().getSimpleName(), userList.size() + " users fetched");
  186. callback.onUsersFetched(userList);
  187. }, errorCallback);
  188. executor.start();
  189. }
  190. public void getCurrentUser(CurrentUserCallback callback, ErrorCallback errorCallback) {
  191. @Language("SQL") String sql = "SELECT * FROM current_user ";
  192. Log.v(getClass().getSimpleName(), "Fetching current_user from db... ");
  193. QueryExecutor executor = new QueryExecutor(sql, res -> {
  194. UserInformation user = null;
  195. int nbRows = 0;
  196. if (res.next()) {
  197. String id = res.getString("id");
  198. String username = res.getString("username");
  199. user = new UserInformation(id, username);
  200. nbRows = 1;
  201. }
  202. Log.v(getClass().getSimpleName(), nbRows + " users fetched");
  203. callback.onFetched(user);
  204. }, errorCallback);
  205. executor.start();
  206. }
  207. /**
  208. * Gets the chat history for a given time frame
  209. *
  210. * @param user1 the user for which to retrieve the history
  211. * @param user2 the user for which to retrieve the history
  212. * @param from the starting date
  213. * @param to the ending date
  214. * @param callback Function called when the request is done
  215. * @param errorCallback The function to call on error
  216. */
  217. public void getChatHistory(UserInformation user1, UserInformation user2, Date from, Date to, HistoryCallback callback, ErrorCallback errorCallback) {
  218. @Language("SQL") String sql =
  219. "SELECT timestamp, sender, recipient, text, file_path " +
  220. "FROM message WHERE (sender = ? OR recipient = ?)" +
  221. " AND timestamp > ? AND timestamp < ? " +
  222. "ORDER BY timestamp";
  223. Log.v(getClass().getSimpleName(), "Fetching chat history from db... ");
  224. executeGetChatHistory(user1, from, to, sql, res -> {
  225. ArrayList<Message> chatHistory = new ArrayList<>();
  226. while (res.next()) {
  227. Date date = new Date(res.getTimestamp("timestamp").getTime());
  228. String text = res.getString("text");
  229. String filePath = res.getString("file_path");
  230. String sId = res.getString("sender");
  231. final UserInformation sender;
  232. final UserInformation recipient;
  233. if (user1.id.equals(sId)) {
  234. sender = user1;
  235. recipient = user2;
  236. } else {
  237. sender = user2;
  238. recipient = user1;
  239. }
  240. if (filePath == null) {
  241. chatHistory.add(new Message(sender, recipient, date, text));
  242. } else {
  243. try {
  244. chatHistory.add(new FileMessage(sender, recipient, date, text, filePath));
  245. } catch (IOException e) {
  246. Log.e(getClass().getSimpleName(), "Error while opening the file", e);
  247. }
  248. }
  249. }
  250. Log.v(getClass().getSimpleName(), chatHistory.size() + " messages fetched");
  251. callback.onHistoryFetched(chatHistory);
  252. }, errorCallback);
  253. }
  254. private void executeGetChatHistory(UserInformation user, Date from, Date to, @Language("SQL") String sql, QueryCallback callback, ErrorCallback errorCallback) {
  255. try {
  256. PreparedStatement preparedStatement = connection.prepareStatement(sql);
  257. preparedStatement.setString(1, user.id);
  258. preparedStatement.setString(2, user.id);
  259. preparedStatement.setLong(3, from.getTime());
  260. preparedStatement.setLong(4, to.getTime());
  261. QueryExecutor executor = new QueryExecutor(
  262. preparedStatement,
  263. callback,
  264. errorCallback);
  265. executor.start();
  266. } catch (SQLException e) {
  267. Log.e(this.getClass().getSimpleName(), "Could not prepare statement: ", e);
  268. errorCallback.onError(e);
  269. }
  270. }
  271. /**
  272. * Adds a message to the database for this user.
  273. * If the user does not exist, we create it.
  274. *
  275. * @param message The message to add to the database
  276. * @param callback Function called when the request is done
  277. * @param errorCallback The function to call on error
  278. */
  279. public void addMessage(Message message, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  280. // Insert the correspondent if not already in the database
  281. Log.v(getClass().getSimpleName(), "Inserting correspondent into db... ");
  282. UserInformation correspondent;
  283. if (CurrentUser.getInstance().getId() != null && CurrentUser.getInstance().getId().equals(message.getSender().id)) {
  284. correspondent = message.getRecipient();
  285. } else {
  286. correspondent = message.getSender();
  287. }
  288. addUser(correspondent, () -> {
  289. // Handle messages containing a file
  290. String filePath = null;
  291. if (message instanceof FileMessage) {
  292. filePath = ((FileMessage) message).getPath();
  293. }
  294. @Language("SQL") String sql = "INSERT INTO message " +
  295. "(timestamp, sender, recipient, text, file_path) VALUES (?, ?, ?, ?, ?)";
  296. Log.v(getClass().getSimpleName(), "Inserting message into db... ");
  297. executeAddMessage(message, filePath, sql, callback, errorCallback);
  298. }, errorCallback);
  299. }
  300. private void executeAddMessage(Message message, String filePath, @Language("SQL") String sql, @Nullable UpdateCallback callback, ErrorCallback errorCallback) {
  301. try {
  302. PreparedStatement preparedStatement = connection.prepareStatement(sql);
  303. preparedStatement.setLong(1, message.getDate().getTime());
  304. preparedStatement.setString(2, message.getSender().id);
  305. preparedStatement.setString(3, message.getRecipient().id);
  306. preparedStatement.setString(4, message.getText());
  307. preparedStatement.setString(5, filePath);
  308. UpdateExecutor executor = new UpdateExecutor(
  309. preparedStatement,
  310. callback,
  311. errorCallback);
  312. executor.start();
  313. } catch (SQLException e) {
  314. Log.e(this.getClass().getSimpleName(), "Could not prepare statement: ", e);
  315. errorCallback.onError(e);
  316. }
  317. }
  318. /**
  319. * Inserts the given user if not existing
  320. *
  321. * @param user The user information to store
  322. * @param callback The function to call on success
  323. * @param errorCallback The function to call on error
  324. */
  325. public void addUser(UserInformation user, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  326. @Language("SQL") String sql;
  327. if (user.getUsername() != null) {
  328. sql = "INSERT OR IGNORE INTO user (id, username) VALUES (?, ?)";
  329. } else {
  330. sql = "INSERT OR IGNORE INTO user (id) VALUES (?)";
  331. }
  332. Log.v(getClass().getSimpleName(), "Adding user to db: " + user.id + " / " + user.getUsername());
  333. executeAddUser(user, sql, callback, errorCallback);
  334. }
  335. public void addCurrentUser(UserInformation user, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  336. @Language("SQL") String sql;
  337. if (user.getUsername() != null) {
  338. sql = "INSERT OR IGNORE INTO current_user (id, username) VALUES (?, ?)";
  339. } else {
  340. sql = "INSERT OR IGNORE INTO current_user (id) VALUES (?)";
  341. }
  342. Log.v(getClass().getSimpleName(), "Adding current_user to db: " + user.id + " / " + user.getUsername());
  343. executeAddUser(user, sql, callback, errorCallback);
  344. }
  345. private void executeAddUser(UserInformation user, @Language("SQL") String sql, @Nullable UpdateCallback callback, ErrorCallback errorCallback) {
  346. try {
  347. PreparedStatement preparedStatement = connection.prepareStatement(sql);
  348. preparedStatement.setString(1, user.id);
  349. if (user.getUsername() != null) {
  350. preparedStatement.setString(2, user.getUsername());
  351. }
  352. UpdateExecutor executor = new UpdateExecutor(
  353. preparedStatement,
  354. callback,
  355. errorCallback);
  356. executor.start();
  357. } catch (SQLException e) {
  358. Log.e(this.getClass().getSimpleName(), "Could not prepare statement: ", e);
  359. errorCallback.onError(e);
  360. }
  361. }
  362. public void updateUsername(UserInformation user, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  363. @Language("SQL") String sql = "UPDATE user SET username = ? where id = ?";
  364. executeUsernameUpdate(user, sql, callback, errorCallback);
  365. }
  366. public void updateCurrentUsername(UserInformation user, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  367. @Language("SQL") String sql = "UPDATE current_user SET username = ? where id = ?";
  368. executeUsernameUpdate(user, sql, callback, errorCallback);
  369. }
  370. private void executeUsernameUpdate(UserInformation user, @Language("SQL") String sql, @Nullable UpdateCallback callback, ErrorCallback errorCallback) {
  371. try {
  372. PreparedStatement preparedStatement = connection.prepareStatement(sql);
  373. preparedStatement.setString(1, user.getUsername());
  374. preparedStatement.setString(2, user.id);
  375. UpdateExecutor executor = new UpdateExecutor(
  376. preparedStatement,
  377. callback,
  378. errorCallback);
  379. executor.start();
  380. } catch (SQLException e) {
  381. Log.e(this.getClass().getSimpleName(), "Could not prepare statement: ", e);
  382. errorCallback.onError(e);
  383. }
  384. }
  385. public interface UsersCallback {
  386. void onUsersFetched(ArrayList<User> users);
  387. }
  388. public interface CurrentUserCallback {
  389. void onFetched(UserInformation user);
  390. }
  391. public interface HistoryCallback {
  392. void onHistoryFetched(ArrayList<Message> history);
  393. }
  394. public interface UpdateCallback {
  395. void onUpdateExecuted();
  396. }
  397. private interface QueryCallback {
  398. void onQueryExecuted(ResultSet resultSet) throws SQLException;
  399. }
  400. private class UpdateExecutor extends Thread {
  401. private final String sqlQuery;
  402. private final PreparedStatement preparedStatement;
  403. private final UpdateCallback callback;
  404. private final ErrorCallback errorCallback;
  405. /**
  406. * Constructs a thread that executes an update on the database
  407. *
  408. * @param sqlQuery The query to execute
  409. * @param callback The function to call on success
  410. * @param errorCallback The function to call on error
  411. */
  412. public UpdateExecutor(@Language("SQL") String sqlQuery, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  413. this.preparedStatement = null;
  414. this.sqlQuery = sqlQuery;
  415. this.callback = callback;
  416. this.errorCallback = errorCallback;
  417. }
  418. public UpdateExecutor(PreparedStatement preparedStatement, @Nullable DatabaseController.UpdateCallback callback, ErrorCallback errorCallback) {
  419. this.preparedStatement = preparedStatement;
  420. this.sqlQuery = null;
  421. this.callback = callback;
  422. this.errorCallback = errorCallback;
  423. }
  424. @Override
  425. public void run() {
  426. try {
  427. int rowsModified;
  428. if (preparedStatement == null) {
  429. Statement statement = connection.createStatement();
  430. rowsModified = statement.executeUpdate(sqlQuery);
  431. statement.close();
  432. } else {
  433. rowsModified = preparedStatement.executeUpdate();
  434. preparedStatement.close();
  435. }
  436. Log.v(getClass().getSimpleName(), rowsModified + " rows modified");
  437. if (callback != null) {
  438. callback.onUpdateExecuted();
  439. }
  440. } catch (SQLException e) {
  441. Log.e(this.getClass().getSimpleName(), "Error executing update: ", e);
  442. errorCallback.onError(e);
  443. }
  444. }
  445. }
  446. private class QueryExecutor extends Thread {
  447. private final String sqlQuery;
  448. private final PreparedStatement preparedStatement;
  449. private final QueryCallback callback;
  450. private final ErrorCallback errorCallback;
  451. /**
  452. * Constructs a thread that executes an update on the database
  453. *
  454. * @param sqlQuery The query to execute
  455. * @param callback The function to call on success
  456. * @param errorCallback The function to call on error
  457. */
  458. public QueryExecutor(@Language("SQL") String sqlQuery, @Nullable QueryCallback callback, ErrorCallback errorCallback) {
  459. this.preparedStatement = null;
  460. this.sqlQuery = sqlQuery;
  461. this.callback = callback;
  462. this.errorCallback = errorCallback;
  463. }
  464. public QueryExecutor(PreparedStatement preparedStatement, @Nullable QueryCallback callback, ErrorCallback errorCallback) {
  465. this.preparedStatement = preparedStatement;
  466. this.sqlQuery = null;
  467. this.callback = callback;
  468. this.errorCallback = errorCallback;
  469. }
  470. @Override
  471. public void run() {
  472. try {
  473. Statement statement = null;
  474. final ResultSet resultSet;
  475. if (preparedStatement == null) {
  476. statement = connection.createStatement();
  477. resultSet = statement.executeQuery(sqlQuery);
  478. } else {
  479. resultSet = preparedStatement.executeQuery();
  480. }
  481. if (callback != null) {
  482. callback.onQueryExecuted(resultSet);
  483. }
  484. resultSet.close();
  485. if (statement != null) {
  486. statement.close();
  487. }
  488. if (preparedStatement != null) {
  489. preparedStatement.close();
  490. }
  491. } catch (SQLException e) {
  492. Log.e(this.getClass().getSimpleName(), "Error executing update: ", e);
  493. errorCallback.onError(e);
  494. }
  495. }
  496. }
  497. }