├── Commons.png ├── architecture.png ├── .gitignore ├── database schema.png ├── src └── main │ └── java │ └── Server │ ├── Database │ ├── Insertable.java │ ├── Table.java │ ├── MessageData │ │ ├── Hashtags.java │ │ ├── Messages.java │ │ ├── Chats.java │ │ └── Tweets.java │ ├── UserData │ │ ├── Countries.java │ │ ├── BlockList.java │ │ ├── Likes.java │ │ ├── Followers.java │ │ └── Users.java │ └── DatabaseController.java │ ├── ServerRunner.java │ ├── Utils │ ├── TweetAnalyzer.java │ ├── Snowflake.java │ ├── Http │ │ └── ServerHttpUtils.java │ └── DBUtils.java │ ├── Controllers │ ├── UserActionsController.java │ └── DataController.java │ ├── StorageUnit │ └── StorageManager.java │ └── ServerRequestHandler.java ├── .idea ├── vcs.xml ├── .gitignore ├── libraries │ ├── common.xml │ ├── Maven__commons_io_commons_io_2_13_0.xml │ ├── Maven__com_google_code_gson_gson_2_10_1.xml │ ├── Maven__io_jsonwebtoken_jjwt_api_0_11_5.xml │ ├── Maven__io_jsonwebtoken_jjwt_impl_0_11_5.xml │ ├── Maven__commons_dbutils_commons_dbutils_1_7.xml │ ├── Maven__io_jsonwebtoken_jjwt_jackson_0_11_5.xml │ ├── Maven__mysql_mysql_connector_java_8_0_28.xml │ ├── Maven__org_apache_commons_commons_lang3_3_12_0.xml │ ├── Maven__com_google_protobuf_protobuf_java_3_11_4.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_core_2_14_2.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_databind_2_12_6_1.xml │ └── Maven__com_fasterxml_jackson_core_jackson_annotations_2_12_6.xml ├── encodings.xml ├── sqldialects.xml ├── modules.xml ├── misc.xml ├── dataSources.xml ├── compiler.xml └── jarRepositories.xml ├── Twitter_Server.iml ├── pom.xml └── README.MD /Commons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farbodbj/Twitter-Server/HEAD/Commons.png -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farbodbj/Twitter-Server/HEAD/architecture.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /out/ 3 | /src/main/java/Server/StorageUnit/tweet_attachments/ 4 | 5 | -------------------------------------------------------------------------------- /database schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farbodbj/Twitter-Server/HEAD/database schema.png -------------------------------------------------------------------------------- /src/main/java/Server/Database/Insertable.java: -------------------------------------------------------------------------------- 1 | package Server.Database; 2 | 3 | public interface Insertable { 4 | boolean insert(T toAdd); 5 | } 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/libraries/common.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/Server/ServerRunner.java: -------------------------------------------------------------------------------- 1 | package Server; 2 | 3 | public class ServerRunner { 4 | public static void main(String[] args) { 5 | //here is written the driver code for starting the server 6 | //and also monitoring server events 7 | ServerRequestHandler requestHandler = new ServerRequestHandler(); 8 | requestHandler.run(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/Table.java: -------------------------------------------------------------------------------- 1 | package Server.Database; 2 | 3 | import org.apache.commons.dbutils.QueryRunner; 4 | 5 | import java.sql.Connection; 6 | import java.sql.SQLException; 7 | 8 | public abstract class Table { 9 | 10 | public static final QueryRunner queryRunner = new QueryRunner(); 11 | protected final Connection conn = DatabaseController.getConnection(); 12 | public abstract void createTable() throws SQLException; 13 | } 14 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://localhost:3306 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__commons_io_commons_io_2_13_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_google_code_gson_gson_2_10_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__io_jsonwebtoken_jjwt_api_0_11_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__io_jsonwebtoken_jjwt_impl_0_11_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__commons_dbutils_commons_dbutils_1_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__io_jsonwebtoken_jjwt_jackson_0_11_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__mysql_mysql_connector_java_8_0_28.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_apache_commons_commons_lang3_3_12_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_google_protobuf_protobuf_java_3_11_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_14_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_12_6_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_12_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/Server/Utils/TweetAnalyzer.java: -------------------------------------------------------------------------------- 1 | package Server.Utils; 2 | 3 | import com.twitter.common.Models.Messages.Textuals.Tweet; 4 | 5 | import java.util.ArrayList; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | //this class is not meant to be instantiated, therefor the constructor is private 10 | public class TweetAnalyzer { 11 | public static final String HASHTAG_RECOGNIZER = "(#+[a-zA-Z0-9(_)]+)"; 12 | 13 | private TweetAnalyzer() {} 14 | 15 | public static String[] getHashtags(Tweet tweet) { 16 | String tweetText = tweet.getText(); 17 | ArrayList hashtags = new ArrayList<>(); 18 | 19 | Pattern pattern = Pattern.compile(HASHTAG_RECOGNIZER, Pattern.CASE_INSENSITIVE); 20 | Matcher matcher = pattern.matcher(tweetText); 21 | 22 | while(matcher.find()) 23 | hashtags.add(matcher.group()); 24 | 25 | return hashtags.toArray(new String[0]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/MessageData/Hashtags.java: -------------------------------------------------------------------------------- 1 | package Server.Database.MessageData; 2 | 3 | 4 | import Server.Database.Table; 5 | import Server.Database.Insertable; 6 | import com.twitter.common.Models.Messages.Textuals.Tweet; 7 | 8 | import java.sql.SQLException; 9 | 10 | public class Hashtags extends Table implements Insertable { 11 | public final static String TABLE_NAME = "Hashtags"; 12 | public final static String COL_HASHTAG = "hashtag"; 13 | public final static String COL_HASHTAG_COUNT = "hashtagCount"; 14 | //private final static Connection conn = DatabaseController.getConnection(); 15 | @Override 16 | public void createTable() throws SQLException { 17 | queryRunner.execute(conn, 18 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 19 | + COL_HASHTAG + " VARCHAR("+ Tweet.MAX_TWEET_LENGTH +") UNIQUE ," 20 | + COL_HASHTAG_COUNT + " INT)"); 21 | } 22 | 23 | @Override 24 | public boolean insert(String toAdd) { 25 | try { 26 | int rowsAffected = 27 | queryRunner.update(conn, 28 | "INSERT INTO "+TABLE_NAME+"("+COL_HASHTAG+", "+COL_HASHTAG_COUNT+")" + 29 | " VALUES (?, ?) ON DUPLICATE KEY UPDATE " + COL_HASHTAG_COUNT + "=" + COL_HASHTAG_COUNT + "+1", 30 | toAdd,1); 31 | 32 | return rowsAffected != 0; 33 | 34 | } catch (SQLException e) { 35 | System.out.println(e.getSQLState()); 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/UserData/Countries.java: -------------------------------------------------------------------------------- 1 | package Server.Database.UserData; 2 | 3 | import Server.Database.DatabaseController; 4 | import Server.Database.Table; 5 | 6 | import java.sql.Connection; 7 | import java.sql.PreparedStatement; 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.util.Locale; 11 | 12 | public class Countries extends Table { 13 | public final static String TABLE_NAME = "Countries"; 14 | public final static String COL_ID = "id"; 15 | public final static String COL_COUNTRY_NAME = "countryName"; 16 | public final static String COL_COUNTRY_CODE = "countryCode"; 17 | //private final static Connection conn = DatabaseController.getConnection(); 18 | 19 | @Override 20 | public void createTable() throws SQLException { 21 | queryRunner.execute(conn, 22 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 23 | + COL_ID + " INT UNIQUE PRIMARY KEY NOT NULL AUTO_INCREMENT," 24 | + COL_COUNTRY_NAME + " CHAR(64) UNIQUE ," 25 | + COL_COUNTRY_CODE + " CHAR(8))"); 26 | //initializeTable(); 27 | } 28 | 29 | private void initializeTable() throws SQLException { 30 | String initQuery = "INSERT IGNORE INTO " + TABLE_NAME + " (" + COL_COUNTRY_NAME + ", " + COL_COUNTRY_CODE + ") VALUES (?, ?)"; 31 | queryRunner.batch( 32 | conn, 33 | initQuery, 34 | generateCountriesList()); 35 | } 36 | 37 | private String[][] generateCountriesList() { 38 | String[] countryCodes = Locale.getISOCountries(); 39 | String[][] countriesList = new String[countryCodes.length][2]; 40 | for (int i = 0; i < countryCodes.length; i++) { 41 | Locale locale = new Locale("", countryCodes[i]); 42 | 43 | countriesList[i][0] = locale.getDisplayCountry(); 44 | countriesList[i][1] = locale.getCountry(); 45 | } 46 | 47 | return countriesList; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Twitter_Server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/UserData/BlockList.java: -------------------------------------------------------------------------------- 1 | package Server.Database.UserData; 2 | 3 | import Server.Database.DatabaseController; 4 | import Server.Database.Table; 5 | 6 | import java.sql.*; 7 | 8 | public class BlockList extends Table 9 | { 10 | public final static String TABLE_NAME = "BlockList"; 11 | public final static String COL_BLOCKED = "blocked"; 12 | public final static String COL_BLOCKER = "blocker"; 13 | //private final static Connection conn = DatabaseController.getConnection(); 14 | @Override 15 | public void createTable() throws SQLException { 16 | queryRunner.execute(conn, 17 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 18 | + COL_BLOCKED + " INT, " 19 | + COL_BLOCKER + " INT, " 20 | +"FOREIGN KEY("+COL_BLOCKED+") REFERENCES " + Users.TABLE_NAME +"("+Users.COL_USERID +"), " 21 | +"FOREIGN KEY("+COL_BLOCKER+") REFERENCES " + Users.TABLE_NAME + "(" + Users.COL_USERID + "), " 22 | +" UNIQUE KEY block_id ("+COL_BLOCKER+", "+COL_BLOCKED+"))"); 23 | 24 | } 25 | 26 | 27 | public boolean insert(int blockerId,int blockedId) { 28 | try { 29 | int insertCount = 30 | queryRunner.update(conn, 31 | "INSERT INTO " +TABLE_NAME+ 32 | "(" + COL_BLOCKED + "," + COL_BLOCKER +")" 33 | + "VALUES (?,?)", 34 | blockedId, 35 | blockerId); 36 | 37 | return insertCount == 1; 38 | } 39 | catch (SQLException e) { 40 | System.out.println(e.getSQLState()); 41 | return false; 42 | } 43 | } 44 | 45 | public boolean remove(int blockerId,int blockedId) { 46 | try { 47 | int rowsAffected = queryRunner.update(conn, 48 | "DELETE FROM " + TABLE_NAME + " WHERE " 49 | + COL_BLOCKED + "= (?) AND " + COL_BLOCKER + "= (?)", 50 | blockedId, 51 | blockerId); 52 | 53 | return rowsAffected == 1; 54 | 55 | } catch (SQLException e) { 56 | System.out.println(e.getSQLState()); 57 | return false; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/UserData/Likes.java: -------------------------------------------------------------------------------- 1 | package Server.Database.UserData; 2 | 3 | 4 | import Server.Database.Table; 5 | import Server.Database.MessageData.Tweets; 6 | 7 | import java.sql.SQLException; 8 | 9 | 10 | public class Likes extends Table 11 | { 12 | public final static String TABLE_NAME = "Likes"; 13 | public final static String COL_TWEET_ID = "tweet";//tweet Id 14 | public final static String COL_LIKER_ID = "Liker";//ID of user that has liked 15 | //private final static Connection conn = DatabaseController.getConnection(); 16 | @Override 17 | public void createTable() throws SQLException { 18 | queryRunner.execute(conn, 19 | "CREATE TABLE IF NOT EXISTS " +TABLE_NAME+"("+ 20 | COL_TWEET_ID + " BIGINT," + 21 | COL_LIKER_ID + " INT, " + 22 | "FOREIGN KEY ("+COL_TWEET_ID+") REFERENCES "+ Tweets.TABLE_NAME +"("+Tweets.COL_TWEET_ID+"), " 23 | + "FOREIGN KEY ("+COL_LIKER_ID+") REFERENCES "+ Users.TABLE_NAME + "("+Users.COL_USERID+"), " 24 | + "UNIQUE KEY like_id ("+COL_LIKER_ID+", "+COL_TWEET_ID+"))"); 25 | } 26 | 27 | public boolean insert(long tweetId ,int likerId) { 28 | try { 29 | int insertCount = 30 | queryRunner.update(conn, 31 | "INSERT INTO " + TABLE_NAME + 32 | "(" + COL_TWEET_ID + "," + COL_LIKER_ID + ")"+ 33 | "VALUES (?,?)", 34 | tweetId, 35 | likerId); 36 | 37 | return insertCount == 1; 38 | } 39 | catch (SQLException e) { 40 | System.out.println(e.getSQLState()); 41 | return false; 42 | } 43 | 44 | 45 | } 46 | public boolean remove(long tweetId ,int likerId) { 47 | try { 48 | int rowsAffected = queryRunner.update(conn, 49 | "DELETE FROM " + TABLE_NAME + " WHERE " + COL_TWEET_ID + "= ? AND " + COL_LIKER_ID + "= ?", 50 | tweetId, 51 | likerId); 52 | 53 | return rowsAffected == 1; 54 | } 55 | catch (SQLException e) 56 | { 57 | System.out.println(e.getSQLState()); 58 | return false; 59 | } 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/MessageData/Messages.java: -------------------------------------------------------------------------------- 1 | package Server.Database.MessageData; 2 | 3 | 4 | import Server.Database.Insertable; 5 | import Server.Database.Table; 6 | import Server.Database.UserData.Users; 7 | import com.twitter.common.Models.Messages.Textuals.Direct; 8 | import java.sql.*; 9 | 10 | 11 | public class Messages extends Table implements Insertable { 12 | 13 | public final static String TABLE_NAME = "Messages"; 14 | public final static String COL_CHAT_ID = "chatId"; 15 | public final static String COL_SENDER_ID = "senderId"; 16 | public final static String COL_MESSAGE_TEXT = "messageText"; 17 | public final static String COL_SENT_AT = "sentAt"; 18 | public final static String COL_IS_RECEIVED = "isReceived"; 19 | //private final static Connection conn = DatabaseController.getConnection(); 20 | 21 | @Override 22 | public void createTable() throws SQLException { 23 | queryRunner.execute(conn, 24 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 25 | + COL_CHAT_ID + " BIGINT," 26 | + COL_SENDER_ID + " INT, " 27 | + COL_MESSAGE_TEXT + " VARCHAR("+Direct.MAX_MESSAGE_LENGTH+"), " 28 | + COL_SENT_AT + " DATETIME, " 29 | + COL_IS_RECEIVED + " BOOLEAN ," 30 | + "FOREIGN KEY("+COL_CHAT_ID+") REFERENCES " + Chats.TABLE_NAME+"("+Chats.COL_CHAT_ID+") ," 31 | + "FOREIGN KEY("+COL_SENDER_ID+") REFERENCES " + Users.TABLE_NAME +"("+Users.COL_USERID +"))"); 32 | } 33 | 34 | @Override 35 | public boolean insert(Direct directMessage) { 36 | try { 37 | int insertCount = 38 | queryRunner.update(conn, 39 | "INSERT INTO " +TABLE_NAME+ "(" + COL_SENDER_ID + "," + 40 | COL_MESSAGE_TEXT + "," + COL_SENT_AT +"," + COL_IS_RECEIVED +")" 41 | + "VALUES (?,?,?,?)", 42 | directMessage.getSender().getUserId(), 43 | directMessage.getMessageText(), 44 | directMessage.getFormattedSentAt(), 45 | false); 46 | 47 | return insertCount == 1; 48 | } 49 | catch (SQLException e) { 50 | System.out.println(e.getSQLState()); 51 | return false; 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/MessageData/Chats.java: -------------------------------------------------------------------------------- 1 | package Server.Database.MessageData; 2 | 3 | 4 | import Server.Database.Insertable; 5 | import Server.Database.Table; 6 | import Server.Database.UserData.Users; 7 | import Server.Utils.DBUtils; 8 | import com.twitter.common.Models.Chat; 9 | import java.sql.*; 10 | 11 | 12 | public class Chats extends Table implements Insertable { 13 | public final static String TABLE_NAME = "Chats"; 14 | public final static String COL_CHAT_ID = "chatId"; 15 | public final static String COL_FIRST_USER = "User1"; 16 | public final static String COL_SECOND_USER = "User2"; 17 | //private final static Connection conn = DatabaseController.getConnection(); 18 | 19 | @Override 20 | public void createTable() throws SQLException { 21 | queryRunner.execute(conn, 22 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 23 | + COL_CHAT_ID + " BIGINT NOT NULL PRIMARY KEY, " 24 | + COL_FIRST_USER + " INT, " 25 | + COL_SECOND_USER + " INT, " 26 | + "FOREIGN KEY("+COL_FIRST_USER+") REFERENCES " + Users.TABLE_NAME +"("+Users.COL_USERID +"), " 27 | + "FOREIGN KEY("+COL_SECOND_USER+") REFERENCES " + Users.TABLE_NAME + "(" + Users.COL_USERID + "))" 28 | ); 29 | 30 | } 31 | 32 | @Override 33 | public boolean insert(Chat chat) { 34 | try { 35 | int insertCount = 36 | queryRunner.update(conn, 37 | "INSERT INTO " + TABLE_NAME + 38 | "("+ COL_CHAT_ID+ ", "+ COL_FIRST_USER + "," + COL_SECOND_USER +")" 39 | + "VALUES (?, ?, ?)", 40 | chat.getChatId(), 41 | chat.getUser1().getUserId(), 42 | chat.getUser2().getUserId()); 43 | 44 | return insertCount == 1; 45 | } 46 | catch (SQLException e) { 47 | System.out.println(e.getSQLState()); 48 | return false; 49 | } 50 | } 51 | 52 | 53 | public Chat selectChat(long chatId) { 54 | try(PreparedStatement preparedStatement = conn.prepareStatement( 55 | "SELECT "+Chats.TABLE_NAME+".*, "+Messages.TABLE_NAME+".* " + 56 | "FROM "+Chats.TABLE_NAME+" " + 57 | "JOIN "+Messages.TABLE_NAME+ 58 | " ON "+Chats.TABLE_NAME+"."+COL_CHAT_ID +"="+ Messages.TABLE_NAME+"."+Messages.COL_CHAT_ID + " " + 59 | "WHERE "+Chats.TABLE_NAME+"."+COL_CHAT_ID +"= (?)")) { 60 | preparedStatement.setLong(1, chatId); 61 | 62 | ResultSet resultSet = preparedStatement.executeQuery(); 63 | 64 | return DBUtils.resultSetToChat(resultSet); 65 | 66 | } catch (SQLException e) { 67 | System.out.println(e.getSQLState()); 68 | return null; 69 | } 70 | } 71 | 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | groupId 8 | Twitter_Server 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 19 13 | 19 14 | UTF-8 15 | 16 | 17 | 18 | 19 | commons-io 20 | commons-io 21 | 2.13.0 22 | 23 | 24 | mysql 25 | mysql-connector-java 26 | 8.0.28 27 | 28 | 29 | io.jsonwebtoken 30 | jjwt-api 31 | 0.11.5 32 | 33 | 34 | io.jsonwebtoken 35 | jjwt-impl 36 | 0.11.5 37 | runtime 38 | 39 | 40 | io.jsonwebtoken 41 | jjwt-jackson 42 | 0.11.5 43 | runtime 44 | 45 | 46 | 47 | com.google.code.gson 48 | gson 49 | 2.10.1 50 | 51 | 52 | org.apache.commons 53 | commons-lang3 54 | 3.12.0 55 | 56 | 57 | com.fasterxml.jackson.core 58 | jackson-core 59 | 2.14.2 60 | 61 | 62 | commons-dbutils 63 | commons-dbutils 64 | 1.7 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 73 | 14 74 | 14 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/Server/Utils/Snowflake.java: -------------------------------------------------------------------------------- 1 | package Server.Utils; 2 | 3 | import java.net.NetworkInterface; 4 | import java.security.SecureRandom; 5 | import java.time.Instant; 6 | import java.util.Enumeration; 7 | 8 | public class Snowflake { 9 | private static final int UNUSED_BITS = 1; // Sign bit, Unused (always set to 0) 10 | private static final int EPOCH_BITS = 41; 11 | private static final int NODE_ID_BITS = 10; 12 | private static final int SEQUENCE_BITS = 12; 13 | private static final long maxNodeId = (1L << NODE_ID_BITS) - 1; 14 | private static final long maxSequence = (1L << SEQUENCE_BITS) - 1; 15 | 16 | // Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z) 17 | private static final long DEFAULT_CUSTOM_EPOCH = 1420070400000L; 18 | private final long nodeId; 19 | private final long customEpoch; 20 | private volatile long lastTimestamp = -1L; 21 | private volatile long sequence = 0L; 22 | 23 | // Create Snowflake with a nodeId and custom epoch 24 | public Snowflake(long nodeId, long customEpoch) { 25 | if(nodeId < 0 || nodeId > maxNodeId) { 26 | throw new IllegalArgumentException(String.format("NodeId must be between %d and %d", 0, maxNodeId)); 27 | } 28 | this.nodeId = nodeId; 29 | this.customEpoch = customEpoch; 30 | } 31 | 32 | // Create Snowflake with a nodeId 33 | public Snowflake(long nodeId) { 34 | this(nodeId, DEFAULT_CUSTOM_EPOCH); 35 | } 36 | 37 | // Let Snowflake generate a nodeId 38 | public Snowflake() { 39 | this.nodeId = createNodeId(); 40 | this.customEpoch = DEFAULT_CUSTOM_EPOCH; 41 | } 42 | 43 | public synchronized long nextId() { 44 | long currentTimestamp = timestamp(); 45 | 46 | if(currentTimestamp < lastTimestamp) { 47 | throw new IllegalStateException("Invalid System Clock!"); 48 | } 49 | 50 | if (currentTimestamp == lastTimestamp) { 51 | sequence = (sequence + 1) & maxSequence; 52 | if(sequence == 0) { 53 | // Sequence Exhausted, wait till next millisecond. 54 | currentTimestamp = waitNextMillis(currentTimestamp); 55 | } 56 | } else { 57 | // reset sequence to start with zero for the next millisecond 58 | sequence = 0; 59 | } 60 | 61 | lastTimestamp = currentTimestamp; 62 | 63 | long id = 64 | currentTimestamp << (NODE_ID_BITS + SEQUENCE_BITS) 65 | | (nodeId << SEQUENCE_BITS) 66 | | sequence; 67 | 68 | return id; 69 | } 70 | 71 | 72 | // Get current timestamp in milliseconds, adjust for the custom epoch. 73 | private long timestamp() { 74 | return Instant.now().toEpochMilli() - customEpoch; 75 | } 76 | 77 | // Block and wait till next millisecond 78 | private long waitNextMillis(long currentTimestamp) { 79 | long current = currentTimestamp; 80 | while (currentTimestamp == lastTimestamp) { 81 | current = timestamp(); 82 | } 83 | return current; 84 | } 85 | 86 | private long createNodeId() { 87 | long nodeId; 88 | try { 89 | StringBuilder sb = new StringBuilder(); 90 | Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); 91 | while (networkInterfaces.hasMoreElements()) { 92 | NetworkInterface networkInterface = networkInterfaces.nextElement(); 93 | byte[] mac = networkInterface.getHardwareAddress(); 94 | if (mac != null) { 95 | for(byte macPort: mac) { 96 | sb.append(String.format("%02X", macPort)); 97 | } 98 | } 99 | } 100 | nodeId = sb.toString().hashCode(); 101 | } catch (Exception ex) { 102 | nodeId = (new SecureRandom().nextInt()); 103 | } 104 | nodeId = nodeId & maxNodeId; 105 | return nodeId; 106 | } 107 | 108 | public long[] parse(long id) { 109 | long maskNodeId = ((1L << NODE_ID_BITS) - 1) << SEQUENCE_BITS; 110 | long maskSequence = (1L << SEQUENCE_BITS) - 1; 111 | 112 | long timestamp = (id >> (NODE_ID_BITS + SEQUENCE_BITS)) + customEpoch; 113 | long nodeId = (id & maskNodeId) >> SEQUENCE_BITS; 114 | long sequence = id & maskSequence; 115 | 116 | return new long[]{timestamp, nodeId, sequence}; 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/main/java/Server/Database/UserData/Followers.java: -------------------------------------------------------------------------------- 1 | package Server.Database.UserData; 2 | 3 | 4 | import Server.Database.Table; 5 | import Server.Utils.DBUtils; 6 | import com.twitter.common.Models.User; 7 | 8 | import java.sql.*; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | 13 | public class Followers extends Table { 14 | public final static String TABLE_NAME = "Followers"; 15 | public final static String COL_FOLLOWED = "followed"; 16 | public final static String COL_FOLLOWER = "follower"; 17 | 18 | @Override 19 | public void createTable() throws SQLException { 20 | queryRunner.execute(conn, 21 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" 22 | + COL_FOLLOWED + " INT, " 23 | + COL_FOLLOWER + " INT, " 24 | + "FOREIGN KEY("+COL_FOLLOWED+") REFERENCES " + Users.TABLE_NAME +"("+Users.COL_USERID +"), " 25 | + "FOREIGN KEY("+COL_FOLLOWER+") REFERENCES " + Users.TABLE_NAME + "(" + Users.COL_USERID + ")," 26 | +" UNIQUE KEY follow_id ("+COL_FOLLOWED+", "+COL_FOLLOWER+"))"); 27 | } 28 | 29 | public boolean insert(int followerId,int followedId) { 30 | try { 31 | int affectedRows = queryRunner.update(conn, 32 | "INSERT INTO " +TABLE_NAME+ "(" + COL_FOLLOWED + "," + COL_FOLLOWER +")" + "VALUES (?,?)", 33 | followedId, 34 | followerId); 35 | 36 | return affectedRows == 1; 37 | } catch (SQLException e) { 38 | System.out.println(e.getSQLState()); 39 | return false; 40 | } 41 | } 42 | 43 | public boolean delete(int followerId, int followedId) { 44 | try { 45 | int affectedRows = queryRunner.update(conn, 46 | "DELETE FROM " +TABLE_NAME+ " WHERE " + COL_FOLLOWED + "= (?) AND " + COL_FOLLOWER + "= (?)", 47 | followedId, 48 | followerId); 49 | 50 | return affectedRows == 1; 51 | } catch (SQLException e) { 52 | System.out.println(e.getSQLState()); 53 | return false; 54 | } 55 | } 56 | 57 | public List selectFollowers(int userID) { 58 | String sqlQuery = generateUserSelectQuery(userID, COL_FOLLOWED, COL_FOLLOWER); 59 | return selectHelper(sqlQuery, userID); 60 | } 61 | 62 | public List selectFollowings(int userID) { 63 | String sqlQuery = generateUserSelectQuery(userID, COL_FOLLOWER, COL_FOLLOWED); 64 | return selectHelper(sqlQuery, userID); 65 | } 66 | 67 | public int selectFollowersCount(int userId) { 68 | return selectCount(COL_FOLLOWER, userId); 69 | } 70 | 71 | public int selectFollowingsCount(int userId) { 72 | return selectCount(COL_FOLLOWED, userId); 73 | } 74 | 75 | 76 | public int selectCount(String columnName, int userId) { 77 | try(PreparedStatement pStmt = conn.prepareStatement("SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE " + columnName + " = ?")) { 78 | pStmt.setInt(1, userId); 79 | 80 | ResultSet resultSet = pStmt.executeQuery(); 81 | if (resultSet.next()) { 82 | int count = resultSet.getInt(1); 83 | resultSet.close(); 84 | return count; 85 | } 86 | 87 | } catch (SQLException e) { 88 | System.out.println(e.getMessage()); 89 | } 90 | 91 | return -1; 92 | } 93 | 94 | private List selectHelper(String sqlQuery, int userID) { 95 | try(PreparedStatement selector = conn.prepareStatement(sqlQuery)) { 96 | selector.setInt(1,userID); 97 | ResultSet rs = selector.executeQuery(); 98 | List users = new ArrayList<>(); 99 | 100 | while (rs.next()) { 101 | users.add(DBUtils.resultSetToUser(rs)); 102 | } 103 | return users; 104 | } 105 | catch (SQLException e) { 106 | System.out.println(e.getSQLState()); 107 | } 108 | return null; 109 | } 110 | 111 | private String generateUserSelectQuery(int userID, String targetColumn, String conditionColumn) { 112 | return "SELECT " 113 | + Users.TABLE_NAME + "." + Users.COL_USERID + ", " 114 | + Users.TABLE_NAME + "." + Users.COL_DISPLAY_NAME + ", " 115 | + Users.TABLE_NAME + "." + Users.COL_USERNAME + ", " 116 | + Users.TABLE_NAME + "." + Users.COL_BIO 117 | + " FROM " + TABLE_NAME 118 | + " JOIN " + Users.TABLE_NAME 119 | + " ON " + targetColumn + "=" + Users.TABLE_NAME + "." + Users.COL_USERID 120 | + " WHERE " + conditionColumn + "= (?)"; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/Server/Controllers/UserActionsController.java: -------------------------------------------------------------------------------- 1 | package Server.Controllers; 2 | 3 | import Server.Database.DatabaseController; 4 | import Server.StorageUnit.StorageManager; 5 | import Server.Utils.Snowflake; 6 | import Server.Utils.TweetAnalyzer; 7 | import com.twitter.common.Exceptions.AttachmentError; 8 | import com.twitter.common.Models.Messages.Textuals.*; 9 | import com.twitter.common.Models.Messages.Visuals.Image; 10 | import com.twitter.common.Models.User; 11 | 12 | import java.util.List; 13 | 14 | 15 | //Singleton class 16 | public class UserActionsController { 17 | private static UserActionsController instance; 18 | private final DatabaseController DBConnection = DatabaseController.getInstance(); 19 | protected final Snowflake ID_GEN = new Snowflake(); 20 | 21 | public static UserActionsController getInstance() { 22 | if(instance != null) 23 | return instance; 24 | 25 | instance = new UserActionsController(); 26 | return instance; 27 | } 28 | 29 | private UserActionsController() {} 30 | 31 | public boolean signUp(User newUser) { 32 | return DBConnection.addUser(newUser); 33 | } 34 | 35 | public User signIn(String username, String passwordHash) { 36 | User user = DBConnection.getUser(username, passwordHash); 37 | if(user != null && user.getUserId() != 0) 38 | user.setProfilePic(StorageManager.loadProfilePhoto(user.getUserId())); 39 | return user; 40 | } 41 | 42 | public boolean follow(int followerId,int followedId) { 43 | return DBConnection.addFollower(followerId,followedId); 44 | } 45 | 46 | public boolean unfollow(int usernameFollower,int usernameFollowed) { 47 | return DBConnection.removeFollowing(usernameFollower,usernameFollowed); 48 | } 49 | 50 | public boolean tweet(Tweet tweet) { 51 | tweet.setTweetId(ID_GEN.nextId()); 52 | try { 53 | StorageManager.saveAttachments(tweet); 54 | } catch (AttachmentError e) { 55 | return false; 56 | } 57 | DBConnection.addHashtags(TweetAnalyzer.getHashtags(tweet)); 58 | return DBConnection.addTweet(tweet); 59 | } 60 | 61 | public boolean quote(Quote quote) { 62 | quote.setTweetId(ID_GEN.nextId()); 63 | try { 64 | StorageManager.saveAttachments(quote); 65 | } catch (AttachmentError e) { 66 | return false; 67 | } 68 | DBConnection.addHashtags(TweetAnalyzer.getHashtags(quote)); 69 | return DBConnection.addQuote(quote); 70 | } 71 | 72 | public boolean mention(Mention mention) { 73 | mention.setTweetId(ID_GEN.nextId()); 74 | try { 75 | StorageManager.saveAttachments(mention); 76 | } catch (AttachmentError e) { 77 | return false; 78 | } 79 | DBConnection.addHashtags(TweetAnalyzer.getHashtags(mention)); 80 | return DBConnection.addMention(mention); 81 | } 82 | 83 | public boolean retweet(Retweet retweet) { 84 | retweet.setTweetId(ID_GEN.nextId()); 85 | DBConnection.addHashtags(TweetAnalyzer.getHashtags(retweet.getRetweeted())); 86 | return DBConnection.addRetweet(retweet); 87 | } 88 | public boolean like(int userId , long tweetId ) { 89 | return DBConnection.addLike(userId,tweetId); 90 | } 91 | 92 | public boolean unlike(int userId , long tweetId ) { 93 | return DBConnection.removeLike(userId,tweetId); 94 | } 95 | 96 | public boolean block(int blockerId, int blockedId) { 97 | DBConnection.removeFollower(blockedId, blockerId); 98 | DBConnection.removeFollowing(blockedId, blockerId); 99 | return DBConnection.addBlocked(blockerId, blockedId); 100 | 101 | } 102 | 103 | public boolean unblock(int blockerId, int blockedId) { 104 | return DBConnection.removeBlocked(blockerId, blockedId); 105 | } 106 | 107 | 108 | public void sendMassage(Direct directMessage) { 109 | DBConnection.sendMassage(directMessage); 110 | } 111 | 112 | public boolean setProfile(int userId, Image newProfile) { 113 | try { 114 | StorageManager.saveProfilePhoto(userId, newProfile); 115 | } catch (AttachmentError e) { 116 | return false; 117 | } 118 | return true; 119 | } 120 | 121 | public boolean setHeader(int userId, Image newHeader) { 122 | try { 123 | StorageManager.saveHeaderPhoto(userId, newHeader); 124 | } catch (AttachmentError e) { 125 | return false; 126 | } 127 | return true; 128 | } 129 | 130 | public boolean setDisplayName(int userId, String newDisplayName) { 131 | return DBConnection.setDisplayName(userId, newDisplayName); 132 | } 133 | 134 | public boolean setBio(int userId, String newBio) { 135 | return DBConnection.setBio(userId, newBio); 136 | } 137 | 138 | public boolean setLocation(int userId, String newLocation) { 139 | return DBConnection.setLocation(userId, newLocation); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/Server/Controllers/DataController.java: -------------------------------------------------------------------------------- 1 | package Server.Controllers; 2 | 3 | import Server.Database.DatabaseController; 4 | import Server.StorageUnit.StorageManager; 5 | import com.twitter.common.Exceptions.AttachmentError; 6 | import com.twitter.common.Models.Chat; 7 | import com.twitter.common.Models.Messages.Textuals.Mention; 8 | import com.twitter.common.Models.Messages.Textuals.Quote; 9 | import com.twitter.common.Models.Messages.Textuals.Retweet; 10 | import com.twitter.common.Models.Messages.Textuals.Tweet; 11 | import com.twitter.common.Models.Timeline; 12 | import com.twitter.common.Models.User; 13 | 14 | import java.io.IOException; 15 | import java.util.List; 16 | 17 | public class DataController //gets timeline and profile and etc. 18 | { 19 | private final DatabaseController DBController = DatabaseController.getInstance(); 20 | private static DataController instance; 21 | 22 | public static DataController getInstance() { 23 | if(instance != null) 24 | return instance; 25 | 26 | instance = new DataController(); 27 | return instance; 28 | } 29 | 30 | public Chat getChat(Chat chat) { 31 | return DBController.getChat(chat); 32 | } 33 | 34 | public User getUser(int userId) { 35 | User user = DBController.getUser(userId); 36 | if(user == null) return null; 37 | 38 | user.setHeaderPic(StorageManager.loadHeaderPhoto(userId)); 39 | user.setProfilePic(StorageManager.loadProfilePhoto(userId)); 40 | return user; 41 | } 42 | 43 | public int getFollowersCount(int userId) {return DBController.getFollowersCount(userId);} 44 | 45 | public List getFollowers(int userId) { 46 | return DBController.getFollowers(userId); 47 | } 48 | 49 | public int getFollowingsCount(int userId) {return DBController.getFollowingsCount(userId);} 50 | 51 | public List getFollowings(int userId) { 52 | return DBController.getFollowings(userId); 53 | } 54 | 55 | public List searchForUser(String searchTerm) { 56 | List users = DBController.searchUsers(searchTerm); 57 | for (User user: users) { 58 | user.setProfilePic(StorageManager.loadProfilePhoto(user.getUserId())); 59 | } 60 | return users; 61 | } 62 | 63 | public Timeline getTimeline(int userId, int MAX_COUNT) { 64 | Timeline timeline = new Timeline(); 65 | timeline.setForUser(userId); 66 | timeline.addAll(DBController.generateTimeline(userId, MAX_COUNT)); 67 | loadTimelineAttachments(timeline); 68 | loadTimelineProfilePhotos(timeline); 69 | return timeline; 70 | } 71 | 72 | private void loadTimelineAttachments(Timeline timeline) { 73 | try { 74 | for (Tweet tweet : timeline.getTimelineTweets()) { 75 | StorageManager.loadAttachments(tweet); 76 | } 77 | } catch (AttachmentError e) { 78 | System.out.println("loading timeline attachments failed."); 79 | //Error handling logic 80 | } 81 | } 82 | 83 | private void loadTimelineProfilePhotos(Timeline timeline) { 84 | //TODO: complete this method 85 | for(Tweet tweet: timeline.getTimelineTweets()) { 86 | tweet 87 | .getSender() 88 | .setProfilePic( 89 | StorageManager.loadProfilePhoto(tweet.getSender().getUserId())); 90 | 91 | 92 | if (tweet instanceof Mention) { 93 | ((Mention) tweet) 94 | .getMentionedTo() 95 | .getSender() 96 | .setProfilePic( 97 | StorageManager.loadProfilePhoto( 98 | ((Mention) tweet) 99 | .getMentionedTo() 100 | .getSender() 101 | .getUserId())); 102 | 103 | } else if (tweet instanceof Quote) { 104 | ((Quote) tweet) 105 | .getQuoted() 106 | .getSender() 107 | .setProfilePic( 108 | StorageManager.loadProfilePhoto( 109 | ((Quote) tweet) 110 | .getQuoted() 111 | .getSender() 112 | .getUserId())); 113 | 114 | } else if (tweet instanceof Retweet) { 115 | ((Retweet) tweet) 116 | .getRetweeted() 117 | .getSender() 118 | .setProfilePic( 119 | StorageManager.loadProfilePhoto( 120 | ((Retweet) tweet) 121 | .getRetweeted() 122 | .getSender() 123 | .getUserId())); 124 | 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Twitter Server 2 | A pure-java implementation of Twitter's server using a REST API and no additional frameworks. 3 | 4 | ## About the project 5 | This project has been done as the final project of the advanced programming course in [AUT](https://www.topuniversities.com/universities/amirkabir-university-technology). 6 | As one of the projects' constraints, use of any backened-related frameworks was not allowed (e.g. Spring Boot etc). 7 | 8 | ## Technologies 9 | For addressing various needs in the Twitter server such as the database, serialization etc, different technologies were researched carefully and then chosen: 10 | 11 |
12 | 13 | ### Database 14 | Regarding the relational nature of most of the saved data in the project, MySQL database was chosen as a robust and scalable SQL database that provides one with many feasible features. Some others databases were also investigated such as PostgreSQL and MongoDB but MySQL still seemed like a better fit for my use. 15 | Querying the database was done using SQL queries only. 16 | 17 | **used libraries:** 18 | 23 | 24 | ### Serialization protocol 25 | As one of the most robust and vastly used serialization protocols, JSON was used, but for larger data structures (photos, videos etc) Apache Commons IO was utilized as the serializer/deserializer. 26 | 27 | **used libraries:** 28 |
29 | • Fasterxml jackson v2.14.2 30 | 31 | • Google gson v2.10.1 32 | 33 |
34 | 35 | ### Request handling 36 | A REST API was designed and implemented from scratch using HttpServer library (built-in to java). For method validation, request format validation, sending responses etc no frameworks were used due to project constraints. 37 | 38 | **used libraries:** 39 |
40 | • HttpServer 41 |
42 | 43 |
44 | 45 | 46 | ## Design and Architecture 47 | 48 | To address various neeeds in the project and also achieving important software engineering principles such as separation of concerns, encapsulation, maintainability, scalability, etc, many different design patterns were used: 49 | 50 | • As the main communication way between the server and the client, [**RSET API**](https://www.digitalocean.com/community/tutorials/restful-web-services-tutorial-java) was used. After the needed methods were identified, related actions were put in the well-defined hierarchy of the API which separated them according to their responsibilities. 51 | An ER schema of the implemented design is shown below: 52 | ![ER schema](https://github.com/farbodbj/Twitter-Server/blob/master/architecture.png) 53 | 54 | • The main design in the server part was [**The Layered architecture**](https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html) as it could best help encapsulate different components and separate the concerns of accessing data (both on disk and database) from concerns of request handling and controlling. 55 | 56 | • Although the [**MVVM design pattern**](https://www.techtarget.com/whatis/definition/Model-View-ViewModel) is mostly related to the client part, but in the whole project (server and client together) the so-called Model components were kept in a module named Commons and were only used as data entities with no buissiness related logic in them. 57 | The UML schema for the commons module can be seen below: 58 | ![Commons module UML](https://github.com/farbodbj/Twitter-Server/blob/master/Commons.png) 59 | 60 | • [**DAO**](https://www.digitalocean.com/community/tutorials/dao-design-pattern) design pattern was effectively applied in the database part to encapsulate the database access logic from other parts. each table in the database has a corresponding class which is the only class that can add, remove or modify the data in that table. 61 | The database schematic can be seen below: 62 | ![database schema](https://github.com/farbodbj/Twitter-Server/blob/master/database%20schema.png) 63 | 64 | ## Implementation details 65 | 66 | ### Authorization 67 |
68 | As suggested in the project description, JSON Web Tokens (known as Jwt) were used for authorizing almost every action a user could take. the access token which were kept in the client would be sent in the header of every request and the server would let the user take the desired action after validating that token. 69 |
70 | 71 | ### Error handling 72 |
73 | Mnay different custom exceptions had been defined in the project which are raised in related situation and they are most often handled by sending the client the appropriate Http status code and message related to that error. 74 |
75 | 76 | ### Handling files 77 |
78 | As the clients could demand sending and recieving different types of video and pictures for various purposes, the need of being able to effectively was handled in the project. In almost all cases, the uniqueness of ids (e.g tweet_id, user_id etc) was used along with a prefix/suffix which showed the kind of file (e.g profile picture, tweet attachment etc). 79 |
80 | 81 | ## Future enhancements 82 | • Using a logger for better monitoring the server 83 | 84 | • Keeping hashed passwords instead of actuall passwords in the database 85 | 86 | • Encrypting the Http connection between the server and the client 87 | 88 | • Putting server configurations in a separate file instead of hardcoding. 89 | -------------------------------------------------------------------------------- /src/main/java/Server/Utils/Http/ServerHttpUtils.java: -------------------------------------------------------------------------------- 1 | package Server.Utils.Http; 2 | 3 | 4 | 5 | import com.google.gson.Gson; 6 | import com.sun.net.httpserver.HttpExchange; 7 | import com.twitter.common.API.ResponseMessage; 8 | import com.twitter.common.API.ResponseModel; 9 | import com.twitter.common.Utils.GsonUtils; 10 | import org.apache.commons.lang3.SerializationUtils; 11 | 12 | import java.io.*; 13 | import java.net.URLDecoder; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static com.twitter.common.API.StatusCode.*; 19 | import static com.twitter.common.Utils.SafeCall.safe; 20 | 21 | 22 | public class ServerHttpUtils { 23 | private final static Gson gson = GsonUtils.getInstance(); 24 | 25 | public static boolean validateEssentialKeys(HttpExchange exchange, Map header, String... keys) { 26 | for (String key: keys) { 27 | if(!header.containsKey(key) || header.get(key).isBlank()){ 28 | badRequest(exchange); 29 | return false; 30 | } 31 | } 32 | return true; 33 | } 34 | 35 | public static boolean validateMethod(String method, HttpExchange exchange) { 36 | if (!method.equals(exchange.getRequestMethod())) { 37 | response( 38 | exchange, 39 | ResponseMessage.METHOD_NOT_ALLOWED, 40 | null, 41 | NOT_ALLOWED, 42 | false); 43 | 44 | return false; 45 | } 46 | return true; 47 | } 48 | 49 | 50 | public static T validateBody(HttpExchange exchange, Class cls) { 51 | T body = parse(exchange, cls); 52 | if (body == null) { 53 | badRequest(exchange); 54 | } 55 | return body; 56 | 57 | } 58 | 59 | public static T validateSerializedBody(HttpExchange exchange, Class cls) { 60 | T body = parseSerialized(exchange, cls); 61 | if(body == null) { 62 | badRequest(exchange); 63 | } 64 | return body; 65 | } 66 | 67 | 68 | public static String getValueFromHeader(HttpExchange exchange, String key) { 69 | return exchange.getRequestHeaders().getFirst(key); 70 | } 71 | 72 | 73 | public static T parse(HttpExchange exchange, Class cls) { 74 | try(InputStreamReader inputStreamReader = new InputStreamReader(exchange.getRequestBody())){ 75 | return gson.fromJson( 76 | inputStreamReader, 77 | cls); 78 | } catch (IOException e) { 79 | System.out.println(ResponseMessage.PARSE_ERROR); 80 | return null; 81 | } 82 | } 83 | 84 | 85 | private static T parseSerialized(HttpExchange exchange, Class cls) { 86 | try (InputStream inputStream = exchange.getRequestBody()) { 87 | byte[] requestBody = inputStream.readAllBytes(); 88 | return SerializationUtils.deserialize(requestBody); 89 | } catch(IOException e) { 90 | System.out.println(ResponseMessage.PARSE_ERROR); 91 | return null; 92 | } 93 | } 94 | 95 | public static void badRequest(HttpExchange exchange) { 96 | response(exchange, ResponseMessage.BAD_REQUEST, null, BAD_REQUEST, false); 97 | } 98 | 99 | public static void internalServerError(HttpExchange exchange) { 100 | response(exchange, ResponseMessage.INTERNAL_SERVER_ERROR, null, UNKNOWN_ERROR, false); 101 | } 102 | 103 | public static void sendErrorResponse(HttpExchange exchange, String message, int statusCode) { 104 | response(exchange, message, null, statusCode, false); 105 | } 106 | 107 | public static void sendSuccessResponse(HttpExchange exchange, String message, T res) { 108 | response(exchange, message, res, SUCCESS, true); 109 | } 110 | 111 | public static void response( 112 | HttpExchange exchange, 113 | String message, 114 | T res, 115 | int code, 116 | boolean success 117 | ) { 118 | try (OutputStream os = exchange.getResponseBody()) { 119 | String response = gson.toJson( 120 | new ResponseModel<>(code, success, message, res) 121 | ); 122 | 123 | exchange.sendResponseHeaders(code, response.getBytes().length); 124 | 125 | os.write(response.getBytes()); 126 | } catch (IOException e) { 127 | System.out.println("failed to send response"); 128 | } 129 | 130 | } 131 | 132 | 133 | public static void serializableResponse( 134 | HttpExchange exchange, 135 | String message, 136 | T res, 137 | int code, 138 | boolean success 139 | ) { 140 | try (OutputStream os = exchange.getResponseBody()) { 141 | ResponseModel response = new ResponseModel<>(code, success, message, res); 142 | byte[] responseBytes = SerializationUtils.serialize((Serializable) response); 143 | exchange.sendResponseHeaders(code, responseBytes.length); 144 | 145 | os.write(responseBytes); 146 | } catch (IOException e) { 147 | System.out.println("failed to send response"); 148 | } 149 | } 150 | 151 | public static Map queryToMap(String query) { 152 | Map result = new HashMap<>(); 153 | if (query == null) { 154 | return result; 155 | } 156 | 157 | for (String param : query.split("&")) { 158 | String[] entry = param.split("="); 159 | if (entry.length > 1) { 160 | safe(() -> entry[1] = URLDecoder.decode(entry[1], StandardCharsets.UTF_8)); 161 | result.put(entry[0], entry[1]); 162 | } else { 163 | result.put(entry[0], ""); 164 | } 165 | } 166 | return result; 167 | } 168 | } -------------------------------------------------------------------------------- /src/main/java/Server/StorageUnit/StorageManager.java: -------------------------------------------------------------------------------- 1 | package Server.StorageUnit; 2 | 3 | import com.twitter.common.Exceptions.AttachmentError; 4 | import com.twitter.common.Models.Messages.Textuals.Tweet; 5 | import com.twitter.common.Models.Messages.Visuals.Image; 6 | import com.twitter.common.Models.Messages.Visuals.Video; 7 | import com.twitter.common.Models.Messages.Visuals.Visual; 8 | 9 | import java.io.File; 10 | import java.io.FilenameFilter; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.nio.file.StandardOpenOption; 16 | import java.util.*; 17 | 18 | import org.apache.commons.io.FilenameUtils; 19 | 20 | import static com.twitter.common.Models.Messages.Visuals.Visual.ALLOWED_IMAGE_FORMAT_EXTENSIONS; 21 | 22 | public class StorageManager { 23 | public final static String STORAGE_UNIT_PATH = "src/main/java/Server/StorageUnit/"; 24 | public final static String TWEET_ATTACHMENTS_PATH = STORAGE_UNIT_PATH + "tweet_attachments/"; 25 | public final static String USER_PICTURES_PATH = STORAGE_UNIT_PATH + "UserPictures/"; 26 | 27 | public static void saveToFile(Path path, byte[] fileBytes) throws IOException { 28 | Files.write(path, fileBytes, StandardOpenOption.CREATE); 29 | } 30 | 31 | public static byte[] loadFromFile(Path path) throws IOException { 32 | return Files.readAllBytes(path); 33 | } 34 | 35 | public static File[] directoryScan(String query, String dir) { 36 | File root = new File(dir); 37 | FilenameFilter beginswithm = (directory, filename) -> filename.startsWith(query); 38 | 39 | return root.listFiles(beginswithm); 40 | } 41 | 42 | public static boolean deleteFileIfExists(String query, String dir) { 43 | File[] matchingFiles = directoryScan(query, dir); 44 | boolean deletedAnyFile = false; 45 | 46 | if (matchingFiles != null) { 47 | for (File file : matchingFiles) { 48 | if (file.delete()) { 49 | deletedAnyFile = true; 50 | } 51 | } 52 | } 53 | 54 | return deletedAnyFile; 55 | } 56 | 57 | public static void saveAttachments(Tweet tweet) throws AttachmentError { 58 | try { 59 | int position = 0; 60 | for (Visual att : tweet.getAttachments()) { 61 | StorageManager.saveToFile(getAttachmentPath(att, tweet.getTweetId(), position++), att.getFileBytes()); 62 | } 63 | } catch (IOException e) { 64 | throw new AttachmentError("saving attachments failed"); 65 | } 66 | } 67 | 68 | 69 | public static void loadAttachments(Tweet tweet) throws AttachmentError { 70 | try { 71 | File[] attachmentFiles = directoryScan(String.valueOf(tweet.getTweetId()), TWEET_ATTACHMENTS_PATH); 72 | LinkedList attachments = new LinkedList<>(); 73 | String fileFormat; 74 | Visual tmp; 75 | for (File file: attachmentFiles) { 76 | fileFormat = FilenameUtils.getExtension(file.getName()); 77 | tmp = (ALLOWED_IMAGE_FORMAT_EXTENSIONS.contains(fileFormat)) ? (new Image()) : (new Video()); 78 | 79 | 80 | tmp.setFileFormat(fileFormat); 81 | tmp.setFileBytes(loadFromFile(file.toPath())); 82 | attachments.add(tmp); 83 | } 84 | 85 | tweet.setAttachments(attachments); 86 | 87 | } catch (IOException e) { 88 | throw new AttachmentError("error loading attachments"); 89 | } 90 | } 91 | 92 | public static void saveProfilePhoto(int userId, Image profilePic) throws AttachmentError { 93 | try { 94 | deleteFileIfExists(userId + "_PP", USER_PICTURES_PATH); 95 | saveToFile( 96 | getProfilePicPath(userId, profilePic), 97 | profilePic.getFileBytes()); 98 | 99 | } catch (IOException e) { 100 | throw new AttachmentError("saving profile picture failed"); 101 | } 102 | } 103 | 104 | public static void saveHeaderPhoto(int userId, Image profilePic) throws AttachmentError { 105 | try { 106 | deleteFileIfExists(userId + "_H", USER_PICTURES_PATH); 107 | saveToFile( 108 | getHeaderPath(userId, profilePic), 109 | profilePic.getFileBytes()); 110 | 111 | } catch (IOException e) { 112 | throw new AttachmentError("saving header picture failed"); 113 | } 114 | } 115 | 116 | public static Image loadProfilePhoto(int userId) { 117 | try { 118 | File[] profilePictureMatches = directoryScan(userId + "_PP", USER_PICTURES_PATH); 119 | if(profilePictureMatches.length == 0) return null; 120 | 121 | return new Image(profilePictureMatches[0]); 122 | } 123 | catch (Exception e) { 124 | System.out.println("file error in loadProfilePhoto"); 125 | } 126 | return null; 127 | } 128 | 129 | 130 | public static Image loadHeaderPhoto(int userId) { 131 | try { 132 | File[] headerPictureMatches = directoryScan(userId + "_H", USER_PICTURES_PATH); 133 | if(headerPictureMatches.length == 0) return null; 134 | 135 | return new Image(headerPictureMatches[0]); 136 | } 137 | catch (Exception e) { 138 | System.out.println("file error in loadProfilePhoto"); 139 | } 140 | return null; 141 | } 142 | 143 | 144 | 145 | private static Path getAttachmentPath(Visual visual, long tweetId, int position) { 146 | String URI = TWEET_ATTACHMENTS_PATH + tweetId + "_" + position + "." + visual.getFileFormat(); 147 | visual.setPathInStorage(URI); 148 | return Paths.get(URI); 149 | } 150 | 151 | private static Path getProfilePicPath(int userId, Image profilePic) { 152 | final String profilePicSuffix = "_PP"; 153 | return getUserPicPath(userId, profilePic, profilePicSuffix); 154 | } 155 | 156 | private static Path getHeaderPath(int userId, Image header) { 157 | final String headerSuffix = "_H"; 158 | return getUserPicPath(userId, header, headerSuffix); 159 | } 160 | 161 | private static Path getUserPicPath(int userId, Image image, String suffix) { 162 | String URI = USER_PICTURES_PATH + userId + suffix + image.getFileFormat(); 163 | image.setPathInStorage(URI); 164 | return Paths.get(URI); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/UserData/Users.java: -------------------------------------------------------------------------------- 1 | package Server.Database.UserData; 2 | 3 | 4 | import Server.Database.Table; 5 | import Server.Database.Insertable; 6 | import Server.Utils.DBUtils; 7 | import com.twitter.common.Models.User; 8 | 9 | import java.sql.*; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import static Server.Utils.DBUtils.resultSetToUser; 14 | import static Server.Utils.DBUtils.resultSetToUserList; 15 | 16 | public class Users extends Table implements Insertable { 17 | 18 | public final static String TABLE_NAME = "Users"; 19 | 20 | public final static String COL_USERID = "userId"; 21 | public final static String COL_USERNAME = "username"; 22 | public final static String COL_DISPLAY_NAME = "displayName"; 23 | public final static String COL_PASSWORD_HASH = "passwordHash"; 24 | public final static String COL_REFRESH_TOKEN = "accessToken"; 25 | public final static String COL_EMAIL = "email"; 26 | public final static String COL_DATE_OF_BIRTH = "dateOfBirth"; 27 | public final static String COL_ACCOUNT_MADE = "accountMade"; 28 | public final static String COL_PROFILE_PIC_PATH = "profilePicPath"; 29 | public final static String COL_HEADER_PIC_PATH = "headerPath"; 30 | public final static String COL_BIO = "bio"; 31 | public final static String COL_LOCATION = "location"; 32 | public final static String COL_COUNTRY_ID = "countryId"; 33 | public final static String COL_PHONE_NUMBER = "phonenumber"; 34 | //private final static Connection conn = DatabaseController.getConnection(); 35 | @Override 36 | public synchronized void createTable() throws SQLException { 37 | queryRunner.execute(conn, 38 | "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" 39 | + COL_USERID + " INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT ," 40 | + COL_USERNAME + " CHAR(255)," 41 | + COL_DISPLAY_NAME + " CHAR(50)," 42 | + COL_PASSWORD_HASH + " CHAR(64)," 43 | + COL_REFRESH_TOKEN + " VARCHAR(2048)," 44 | + COL_EMAIL + " CHAR(255)," 45 | + COL_DATE_OF_BIRTH + " DATE," 46 | + COL_ACCOUNT_MADE + " DATE," 47 | + COL_BIO + " CHAR(160)," 48 | + COL_LOCATION + " CHAR(50)," 49 | + COL_COUNTRY_ID + " INT, " 50 | + "FOREIGN KEY ("+COL_COUNTRY_ID+") REFERENCES "+ Countries.TABLE_NAME +" ("+Countries.COL_ID+")," 51 | + COL_PHONE_NUMBER + " CHAR(32))"); 52 | } 53 | 54 | @Override 55 | public synchronized boolean insert(User toAdd) { 56 | try { 57 | int insertCount = 58 | queryRunner.update(conn, 59 | "INSERT INTO " + TABLE_NAME + "(" 60 | + COL_DISPLAY_NAME + ", " + COL_USERNAME + ", " 61 | + COL_PASSWORD_HASH + ", " + COL_EMAIL + ", " 62 | + COL_DATE_OF_BIRTH + ", " + COL_ACCOUNT_MADE + ")" + 63 | " VALUES (?, ?, ?, ?, ?, ?)", 64 | toAdd.getDisplayName(), 65 | toAdd.getUsername(), 66 | toAdd.getPasswordHash(), 67 | toAdd.getEmail(), 68 | toAdd.getDateOfBirth(), 69 | toAdd.getAccountMade()); 70 | 71 | return insertCount == 1; 72 | } catch (SQLException e) { 73 | System.out.println(e.getMessage()); 74 | return false; 75 | } 76 | } 77 | 78 | public synchronized User selectUser(int userId) { 79 | try(PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM " + Users.TABLE_NAME + " WHERE " + COL_USERID + "= (?)")) { 80 | pStmt.setInt(1, userId); 81 | 82 | ResultSet rs = pStmt.executeQuery(); 83 | 84 | if(rs.next()) 85 | return resultSetToUser(rs); 86 | return null; 87 | 88 | } catch (SQLException e) { 89 | System.out.println(e.getMessage()); 90 | return null; 91 | } 92 | } 93 | 94 | 95 | public synchronized User selectUser(String username, String passwordHash) { 96 | try(PreparedStatement pStmt = conn.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE (" + COL_USERNAME + " = (?) OR " + COL_EMAIL + "= (?)) AND " + COL_PASSWORD_HASH + " = (?)")) { 97 | pStmt.setString(1, username); 98 | pStmt.setString(2, username); 99 | pStmt.setString(3, passwordHash); 100 | 101 | ResultSet rs = pStmt.executeQuery(); 102 | if(rs.next()) 103 | return resultSetToUser(rs); 104 | return null; 105 | 106 | } catch (SQLException e) { 107 | System.out.println(e.getMessage()); 108 | return null; 109 | } 110 | } 111 | 112 | public synchronized List searchUser(String searchTerm) { 113 | try(PreparedStatement pStmt = conn.prepareStatement( 114 | "SELECT " + 115 | COL_USERID + ", " + 116 | COL_DISPLAY_NAME + ", " + 117 | COL_USERNAME + ", " + 118 | COL_BIO + 119 | " FROM " + TABLE_NAME + 120 | " WHERE " + COL_DISPLAY_NAME + " LIKE CONCAT('%', ?, '%') OR " + 121 | COL_USERNAME + " LIKE CONCAT('%', ?, '%')")) { 122 | 123 | pStmt.setString(1, searchTerm); 124 | pStmt.setString(2, searchTerm); 125 | 126 | ResultSet resultSet = pStmt.executeQuery(); 127 | 128 | return resultSetToUserList(resultSet); 129 | 130 | } catch (SQLException e) { 131 | System.out.println(e.getMessage()); 132 | } 133 | 134 | return null; 135 | } 136 | 137 | public synchronized boolean exists(String columnName, String value) { 138 | try (PreparedStatement pStmt = conn.prepareStatement("SELECT COUNT(*) "+columnName+" FROM " + TABLE_NAME + " WHERE " + columnName + " = (?)")) { 139 | pStmt.setString(1 ,value); 140 | try (ResultSet rs = pStmt.executeQuery()) { 141 | if (rs.next()) { 142 | return rs.getInt(columnName)>0; 143 | } 144 | } 145 | } catch (SQLException e) { 146 | System.out.println(e.getMessage()); 147 | } 148 | 149 | return false; 150 | } 151 | 152 | public synchronized boolean updateColumn(int userId, String newValue, String column) { 153 | try { 154 | int updateCount = queryRunner.update(conn, 155 | "UPDATE " + TABLE_NAME + " SET " + column + "=(?) " + 156 | "WHERE " + COL_USERID + "=(?)", 157 | newValue, 158 | userId); 159 | 160 | return updateCount == 1; 161 | } catch (SQLException e) { 162 | System.out.println(e.getMessage()); 163 | } 164 | return false; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/DatabaseController.java: -------------------------------------------------------------------------------- 1 | package Server.Database; 2 | 3 | import Server.Database.MessageData.*; 4 | import Server.Database.UserData.*; 5 | import com.twitter.common.Models.Chat; 6 | import com.twitter.common.Models.Messages.Textuals.*; 7 | import com.twitter.common.Models.User; 8 | 9 | import java.sql.*; 10 | import java.util.*; 11 | 12 | 13 | //Singleton class 14 | public class DatabaseController { 15 | private final String DB_URL = "jdbc:mysql://localhost/twitter"; 16 | private final String DB_USERNAME = "root"; 17 | private final String DB_PASS = "AP_Twitter"; 18 | private static Connection conn; 19 | private Tweets tweets; 20 | private Followers followers ; 21 | private Users users; 22 | private BlockList blockList; 23 | private Hashtags hashtags; 24 | private Countries countries; 25 | private Likes likes; 26 | private Chats chats; 27 | private Messages messages; 28 | private static DatabaseController instance; 29 | 30 | public static DatabaseController getInstance() { 31 | if(instance != null) 32 | return instance; 33 | 34 | instance = new DatabaseController(); 35 | return instance; 36 | } 37 | 38 | private DatabaseController() { 39 | try { 40 | //establish database connection 41 | conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASS); 42 | } catch (SQLException e) { 43 | System.out.println(e.getSQLState()); 44 | } 45 | } 46 | 47 | public void initializeDB() throws SQLException { 48 | hashtags = new Hashtags(); 49 | countries = new Countries(); 50 | users = new Users(); 51 | followers = new Followers(); 52 | tweets = new Tweets(); 53 | blockList = new BlockList(); 54 | chats = new Chats(); 55 | messages = new Messages(); 56 | likes = new Likes(); 57 | 58 | hashtags.createTable(); 59 | countries.createTable(); 60 | users.createTable(); 61 | followers.createTable(); 62 | tweets.createTable(); 63 | blockList.createTable(); 64 | chats.createTable(); 65 | messages.createTable(); 66 | likes.createTable(); 67 | } 68 | 69 | public static Connection getConnection() { 70 | return conn; 71 | } 72 | 73 | public boolean addUser(User user) { 74 | return users.insert(user); 75 | } 76 | 77 | public User getUser(String username, String passwordHash) { 78 | return users.selectUser(username, passwordHash); 79 | } 80 | 81 | public User getUser(int userId) { 82 | return users.selectUser(userId); 83 | } 84 | 85 | public boolean addFollower(int followerId, int followedId) { 86 | if(users.selectUser(followerId) == null || users.selectUser(followedId) == null) 87 | return false; 88 | return followers.insert(followerId, followedId); 89 | } 90 | 91 | public boolean removeFollowing(int followerId, int followedId) { 92 | return followers.delete(followerId, followedId); 93 | } 94 | 95 | public boolean removeFollower(int followerId, int followedId) { 96 | return followers.delete(followedId, followerId); 97 | } 98 | 99 | public boolean addTweet(Tweet tweet) { 100 | return tweets.insert(tweet); 101 | } 102 | 103 | public boolean addQuote(Quote quote) { 104 | return tweets.insert(quote); 105 | } 106 | 107 | public boolean addMention(Mention mention) { 108 | return tweets.insert(mention) && tweets.incrementMentions(mention); 109 | } 110 | 111 | public boolean addRetweet(Retweet retweet) { 112 | return tweets.insert(retweet) && tweets.incrementRetweets(retweet); 113 | } 114 | 115 | public boolean addLike(int userId , long tweetId) { 116 | tweets.incrementLikes(tweetId); 117 | return likes.insert(tweetId, userId); 118 | } 119 | 120 | public boolean removeLike(int userId , long tweetId) { 121 | tweets.reduceLikes(tweetId); 122 | return likes.remove(tweetId, userId); 123 | } 124 | 125 | public boolean addHashtags(String[] hashtagArray) { 126 | for (String hashtag: hashtagArray) 127 | if(!hashtags.insert(hashtag)) 128 | return false; 129 | return true; 130 | } 131 | 132 | public boolean addBlocked(int blockerId, int blockedId) { 133 | return blockList.insert(blockerId, blockedId); 134 | } 135 | 136 | public boolean removeBlocked(int blockerId, int blockedId) { 137 | return blockList.remove(blockerId, blockedId); 138 | } 139 | 140 | 141 | public List generateTimeline(int userId, int MAX_COUNT) { 142 | List timeline = tweets.selectTimelineTweets(userId, MAX_COUNT); 143 | timeline.addAll(tweets.selectTimelineRetweets(userId, MAX_COUNT)); 144 | timeline.addAll(tweets.selectTimelineQuotes(userId, MAX_COUNT)); 145 | timeline.addAll(tweets.selectTimelineMentions(userId, MAX_COUNT)); 146 | timeline.addAll(tweets.selectTimelineFavStars()); 147 | 148 | timeline.sort(Comparator.comparing(Tweet::getSentAt).reversed()); 149 | //TODO: delete possible duplicates 150 | 151 | return timeline; 152 | } 153 | 154 | public Map getHashtagReport() { 155 | //TODO: just a simple query from db 156 | return null; 157 | } 158 | 159 | public boolean emailExists(String email) { 160 | return users.exists(Users.COL_EMAIL, email); 161 | } 162 | 163 | public boolean usernameExists(String username) { 164 | return users.exists(Users.COL_USERNAME, username); 165 | } 166 | 167 | public void sendMassage(Direct directMessage) { 168 | messages.insert(directMessage); 169 | } 170 | 171 | public void addChat(Chat chat) { 172 | chats.insert(chat); 173 | } 174 | 175 | public Chat getChat(Chat chat) { 176 | return chats.selectChat(chat.getChatId()); 177 | } 178 | 179 | public List searchUsers(String searchTerm) { 180 | return users.searchUser(searchTerm); 181 | } 182 | 183 | public List getFollowers(int userId) { 184 | return followers.selectFollowers(userId); 185 | } 186 | 187 | public List getFollowings(int userId) { 188 | return followers.selectFollowings(userId); 189 | } 190 | 191 | public int getFollowersCount(int userId) {return followers.selectFollowersCount(userId);} 192 | 193 | public int getFollowingsCount(int userId) {return followers.selectFollowingsCount(userId);} 194 | 195 | public boolean setDisplayName(int userId, String newDisplayName) { 196 | return users.updateColumn(userId, newDisplayName, Users.COL_DISPLAY_NAME); 197 | } 198 | 199 | public boolean setBio(int userId, String newBio) { 200 | return users.updateColumn(userId, newBio, Users.COL_BIO); 201 | } 202 | 203 | public boolean setLocation(int userId, String newLocation) { 204 | return users.updateColumn(userId, newLocation, Users.COL_LOCATION); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/Server/Utils/DBUtils.java: -------------------------------------------------------------------------------- 1 | package Server.Utils; 2 | 3 | import Server.Database.MessageData.Chats; 4 | import Server.Database.MessageData.Messages; 5 | import Server.Database.MessageData.Tweets; 6 | import Server.Database.UserData.Users; 7 | import com.twitter.common.Models.Chat; 8 | import com.twitter.common.Models.Messages.Textuals.*; 9 | import com.twitter.common.Models.User; 10 | 11 | import java.sql.ResultSet; 12 | import java.sql.ResultSetMetaData; 13 | import java.sql.SQLException; 14 | import java.time.LocalDateTime; 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.function.BiFunction; 20 | 21 | public class DBUtils { 22 | public static List> resultSetToList(ResultSet rs) { 23 | try { 24 | ResultSetMetaData md = rs.getMetaData(); 25 | int columns = md.getColumnCount(); 26 | List> list = new ArrayList<>(); 27 | 28 | while (rs.next()) { 29 | HashMap row = new HashMap<>(columns); 30 | for (int i = 1; i <= columns; ++i) { 31 | row.put(md.getColumnLabel(i), rs.getObject(i)); 32 | } 33 | list.add(row); 34 | } 35 | 36 | return list; 37 | } catch (SQLException e) { 38 | System.out.println(e.getSQLState()); 39 | } 40 | 41 | return null; 42 | } 43 | 44 | public static Map resultSetToHashMap(ResultSet rs) { 45 | HashMap row = null; 46 | try { 47 | ResultSetMetaData md = rs.getMetaData(); 48 | int columns = md.getColumnCount(); 49 | row = new HashMap<>(columns); 50 | 51 | for (int i = 1; i <= columns; ++i) 52 | row.put(md.getColumnName(i), rs.getObject(i)); 53 | 54 | } catch (SQLException e) { 55 | System.out.println(e.getSQLState()); 56 | } 57 | 58 | return row; 59 | } 60 | 61 | public static User resultSetToUser(ResultSet rs) throws SQLException { 62 | User user = new User(); 63 | try { 64 | user.setUserId(rs.getInt(Users.COL_USERID)); 65 | user.setDisplayName(rs.getString(Users.COL_DISPLAY_NAME)); 66 | user.setUsername(rs.getString(Users.COL_USERNAME)); 67 | user.setEmail(rs.getString(Users.COL_EMAIL)); 68 | user.setDateOfBirth(rs.getDate(Users.COL_DATE_OF_BIRTH)); 69 | user.setAccountMade(rs.getDate(Users.COL_ACCOUNT_MADE)); 70 | user.setBio(rs.getString(Users.COL_BIO)); 71 | user.setLocation(rs.getString(Users.COL_LOCATION)); 72 | } catch (SQLException ignore) { 73 | //column not found should be ignored since not all columns are needed 74 | } 75 | return user; 76 | } 77 | 78 | public static List resultSetToUserList(ResultSet resultSet) { 79 | List userList = new ArrayList<>(); 80 | try { 81 | while (resultSet.next()) { 82 | User user = resultSetToUser(resultSet); 83 | userList.add(user); 84 | } 85 | 86 | return userList; 87 | 88 | } catch (SQLException e) { 89 | System.out.println(e.getMessage()); 90 | } 91 | return null; 92 | } 93 | 94 | public static Chat resultSetToChat(ResultSet rs) throws SQLException { 95 | Chat chat = new Chat( 96 | rs.getInt(Chats.COL_FIRST_USER), 97 | rs.getInt(Chats.COL_SECOND_USER)); 98 | 99 | while(rs.next()) { 100 | chat.addDM(resultSetToMessage(rs)); 101 | } 102 | 103 | return chat; 104 | } 105 | 106 | public static Direct resultSetToMessage(ResultSet rs) throws SQLException { 107 | Direct directMessage = new Direct(); 108 | int senderID = Integer.parseInt(rs.getString(Messages.COL_SENDER_ID)); 109 | String messageText = rs.getString(Messages.COL_MESSAGE_TEXT); 110 | String sentAt = rs.getString(Messages.COL_SENT_AT); 111 | boolean isReceived = Boolean.parseBoolean(rs.getString(Messages.COL_IS_RECEIVED)); 112 | 113 | directMessage.getSender().setUserId(senderID); 114 | directMessage.setMessageText(messageText); 115 | directMessage.setFormattedSentAt(sentAt); 116 | directMessage.setReceived(isReceived); 117 | 118 | return directMessage; 119 | } 120 | 121 | 122 | public static List timelineTweetsHandler(List> tweets) { 123 | List timelineTweets = new ArrayList<>(); 124 | for (HashMap tweetHashMap: tweets) { 125 | User user = new User(); 126 | Tweet tweet = new Tweet(); 127 | 128 | user.setUsername((String) tweetHashMap.get(Users.COL_USERNAME)); 129 | user.setDisplayName((String) tweetHashMap.get(Users.COL_DISPLAY_NAME)); 130 | user.setUserId((int) tweetHashMap.get(Users.COL_USERID)); 131 | 132 | tweet.setTweetId((long) tweetHashMap.get(Tweets.COL_TWEET_ID)); 133 | tweet.setText((String) tweetHashMap.get(Tweets.COL_TEXT)); 134 | tweet.setFavCount((int) tweetHashMap.get(Tweets.COL_FAV_COUNT)); 135 | tweet.setRetweetCount((int) tweetHashMap.get(Tweets.COL_RETWEET_COUNT)); 136 | tweet.setMentionCount((int) tweetHashMap.get(Tweets.COL_MENTION_COUNT)); 137 | tweet.setSentAt((LocalDateTime) tweetHashMap.get(Tweets.COL_SENT_AT)); 138 | 139 | tweet.setSender(user); 140 | timelineTweets.add(tweet); 141 | } 142 | return timelineTweets; 143 | } 144 | 145 | public static List timelineRetweetsHandler(List> retweets) { 146 | List retweetList = new ArrayList<>(); 147 | 148 | for (HashMap retweetMap : retweets) { 149 | User retweeter = new User(); 150 | User originalUser = new User(); 151 | Tweet originalTweet = new Tweet(); 152 | Retweet retweet = new Retweet(); 153 | 154 | retweeter.setUserId((int) retweetMap.get("retweeter_user_id")); 155 | retweeter.setDisplayName((String) retweetMap.get("retweeter_display_name")); 156 | 157 | originalUser.setUserId((int) retweetMap.get(Users.COL_USERID)); 158 | originalUser.setDisplayName((String) retweetMap.get(Users.COL_DISPLAY_NAME)); 159 | originalUser.setUsername((String) retweetMap.get(Users.COL_USERNAME)); 160 | 161 | originalTweet.setSender(originalUser); 162 | originalTweet.setTweetId((long) retweetMap.get(Tweets.COL_TWEET_ID)); 163 | originalTweet.setText((String) retweetMap.get(Tweets.COL_TEXT)); 164 | originalTweet.setFavCount((int) retweetMap.get(Tweets.COL_FAV_COUNT)); 165 | originalTweet.setRetweetCount((int) retweetMap.get(Tweets.COL_RETWEET_COUNT)); 166 | originalTweet.setMentionCount((int) retweetMap.get(Tweets.COL_MENTION_COUNT)); 167 | originalTweet.setSentAt(((LocalDateTime) retweetMap.get(Tweets.COL_SENT_AT))); 168 | 169 | retweet.setSentAt((LocalDateTime) retweetMap.get(Tweets.COL_SENT_AT)); 170 | retweet.setRetweeted(originalTweet); 171 | retweet.setSender(retweeter); 172 | 173 | 174 | retweetList.add(retweet); 175 | } 176 | 177 | return retweetList; 178 | } 179 | 180 | public static List timelineQuoteHandler(List> quotes) { 181 | return timelineHandler(quotes, Quote::new, Tweets.QUOTER_ALIAS); 182 | } 183 | 184 | public static List timelineMentionHandler(List> mentions) { 185 | return timelineHandler(mentions, Mention::new, Tweets.MENTIONER_ALIAS); 186 | } 187 | 188 | public static List timelineHandler(List> rows, BiFunction tweetConstructor, String senderPrefix) { 189 | List tweets = new ArrayList<>(); 190 | 191 | for (HashMap row : rows) { 192 | User originalUser = new User(); 193 | User sender = new User(); 194 | Tweet originalTweet = new Tweet(); 195 | T tweet = tweetConstructor.apply(sender, originalTweet); 196 | 197 | originalUser.setUserId((int) row.get("original_user_id")); 198 | originalUser.setDisplayName((String) row.get("original_display_name")); 199 | originalUser.setUsername((String) row.get("original_username")); 200 | 201 | sender.setUserId((int) row.get(senderPrefix + "_user_id")); 202 | sender.setDisplayName((String) row.get(senderPrefix + "_display_name")); 203 | sender.setUsername((String) row.get(senderPrefix + "_username")); 204 | 205 | originalTweet.setSender(originalUser); 206 | originalTweet.setTweetId((long) row.get("original_tweet_id")); 207 | originalTweet.setText((String) row.get("original_text")); 208 | originalTweet.setFavCount((int) row.get("original_fav_count")); 209 | originalTweet.setRetweetCount((int) row.get("original_retweet_count")); 210 | originalTweet.setMentionCount((int) row.get("original_mention_count")); 211 | originalTweet.setSentAt(((LocalDateTime) row.get("original_sent_at"))); 212 | 213 | tweet.setSender(sender); 214 | tweet.setTweetId((long) row.get(senderPrefix + "_tweet_id")); 215 | tweet.setText((String) row.get(senderPrefix + "_text")); 216 | tweet.setFavCount((int) row.get(senderPrefix + "_fav_count")); 217 | tweet.setRetweetCount((int) row.get(senderPrefix + "_retweet_count")); 218 | tweet.setMentionCount((int) row.get(senderPrefix + "_mention_count")); 219 | tweet.setSentAt(((LocalDateTime) row.get(senderPrefix + "_sent_at"))); 220 | 221 | tweets.add(tweet); 222 | } 223 | 224 | return tweets; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/Server/Database/MessageData/Tweets.java: -------------------------------------------------------------------------------- 1 | package Server.Database.MessageData; 2 | 3 | import Server.Database.*; 4 | import Server.Database.UserData.Followers; 5 | import Server.Database.UserData.Users; 6 | import Server.Utils.DBUtils; 7 | import com.twitter.common.Models.Messages.Textuals.Mention; 8 | import com.twitter.common.Models.Messages.Textuals.Quote; 9 | import com.twitter.common.Models.Messages.Textuals.Retweet; 10 | import com.twitter.common.Models.Messages.Textuals.Tweet; 11 | import java.sql.*; 12 | import java.util.List; 13 | 14 | 15 | public class Tweets extends Table implements Insertable { 16 | public final static int FAV_STAR_MIN = 10; 17 | public final static String TABLE_NAME = "Tweets"; 18 | public final static String COL_TWEET_ID = "tweetId"; 19 | public final static String COL_SENDER_ID = "senderId"; 20 | public final static String COL_TEXT = "Tweettext"; 21 | public final static String COL_FAV_COUNT = "favCount"; 22 | public final static String COL_RETWEET_COUNT = "retweetCount"; 23 | public final static String COL_MENTION_COUNT = "mentionCount"; 24 | public final static String COL_SENT_AT = "sentAt"; 25 | public final static String COL_PARENT_TWEET = "parentTweet"; 26 | public final static String COL_TWEET_TYPE = "tweetType"; 27 | public final static String TWEET_TYPE_ENUM = "ENUM('Tweet', 'Retweet', 'Mention', 'Quote')"; 28 | public final static String MENTIONER_ALIAS = "mentioner"; 29 | public final static String MENTIONED_ALIAS = "mentioned"; 30 | public final static String QUOTER_ALIAS = "quoter"; 31 | public final static String QUOTED_ALIAS = "quoted"; 32 | //private final static Connection conn = DatabaseController.getConnection(); 33 | enum TweetType {Tweet, Retweet, Mention, Quote} 34 | 35 | @Override 36 | public void createTable() throws SQLException{ 37 | queryRunner.execute(conn, 38 | "CREATE TABLE IF NOT EXISTS "+ TABLE_NAME + "(" 39 | + COL_TWEET_ID + " BIGINT NOT NULL UNIQUE PRIMARY KEY," 40 | + COL_SENDER_ID + " INT NOT NULL, " 41 | + "FOREIGN KEY(" + COL_SENDER_ID + ") REFERENCES "+ Users.TABLE_NAME+"("+ Users.COL_USERID + ")," 42 | + COL_TEXT + " VARCHAR("+Tweet.MAX_TWEET_LENGTH+") ," 43 | + COL_FAV_COUNT + " INT DEFAULT 0," //maybe add NOT NULL some day? 44 | + COL_RETWEET_COUNT + " INT DEFAULT 0," 45 | + COL_MENTION_COUNT + " INT DEFAULT 0," 46 | + COL_SENT_AT + " DATETIME NOT NULL, " 47 | + COL_PARENT_TWEET + " BIGINT REFERENCES "+COL_TWEET_ID + ", " 48 | + COL_TWEET_TYPE + " " + TWEET_TYPE_ENUM +" NOT NULL "+")"); 49 | } 50 | 51 | 52 | public boolean insert(Tweet tweet) { 53 | return insertWithParams( 54 | tweet, 55 | 0, 56 | 0, 57 | 0, 58 | 0L, 59 | TweetType.Tweet); 60 | } 61 | 62 | public boolean insert(Mention mention) { 63 | return insertWithParams( 64 | mention, 65 | 0, 66 | 0, 67 | 0, 68 | mention.getMentionedTo().getTweetId(), 69 | TweetType.Mention); 70 | } 71 | 72 | public boolean insert(Quote quote) { 73 | return insertWithParams(quote, 74 | 0, 75 | 0, 76 | 0, 77 | quote.getQuoted().getTweetId(), 78 | TweetType.Quote); 79 | } 80 | 81 | public boolean insert(Retweet retweet) { 82 | return insertWithParams( 83 | retweet, 84 | 0, 85 | 0, 86 | 0, 87 | retweet.getRetweeted().getTweetId(), 88 | TweetType.Retweet); 89 | } 90 | 91 | private boolean insertWithParams(Tweet tweet, 92 | int favCount, 93 | int retweetCount, 94 | int mentionCount, 95 | long parentTweet, 96 | TweetType tweetType) { 97 | try { 98 | int rowsUpdated = queryRunner.update(conn, 99 | "INSERT INTO " + TABLE_NAME + "(" 100 | + COL_TWEET_ID + "," 101 | + COL_SENDER_ID + "," 102 | + COL_TEXT + "," 103 | + COL_FAV_COUNT + "," 104 | + COL_RETWEET_COUNT + "," 105 | + COL_MENTION_COUNT + "," 106 | + COL_SENT_AT + ", " 107 | + COL_PARENT_TWEET + ", " 108 | + COL_TWEET_TYPE + ") " 109 | + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", 110 | tweet.getTweetId(), 111 | tweet.getSender().getUserId(), 112 | tweet.getText(), 113 | favCount, 114 | retweetCount, 115 | mentionCount, 116 | tweet.getFormattedSentAt(), 117 | parentTweet, 118 | tweetType.name()); 119 | 120 | return rowsUpdated == 1; 121 | } catch (SQLException e) { 122 | System.out.println(e.getSQLState()); 123 | return false; 124 | } 125 | } 126 | 127 | private int findParent(long retweetId) 128 | { 129 | try (PreparedStatement pStmt = conn.prepareStatement("SELECT " + COL_PARENT_TWEET + " FROM " + TABLE_NAME + " WHERE "+ COL_TWEET_ID + "= (?)")) 130 | { 131 | int parentId; 132 | pStmt.setLong(1,retweetId); 133 | ResultSet rs = pStmt.executeQuery(); 134 | if(rs.next()) 135 | { 136 | parentId = rs.getInt(COL_PARENT_TWEET); 137 | return parentId; 138 | } 139 | } 140 | catch (SQLException e) { 141 | System.out.println(e.getSQLState()); 142 | } 143 | return 0; 144 | } 145 | private String findTweetType(long tweetId) 146 | { 147 | try (PreparedStatement pStmt = conn.prepareStatement("SELECT " + COL_TWEET_TYPE + " FROM " + TABLE_NAME + " WHERE "+ COL_TWEET_ID + "= (?)")) 148 | { 149 | String type ; 150 | pStmt.setLong(1,tweetId); 151 | ResultSet rs = pStmt.executeQuery(); 152 | if(rs.next()) 153 | { 154 | type = rs.getString(COL_TWEET_TYPE); 155 | return type; 156 | } 157 | } 158 | catch (SQLException e) { 159 | System.out.println(e.getSQLState()); 160 | } 161 | return null; 162 | } 163 | 164 | public synchronized void reduceLikes(long tweetId) { 165 | if (findTweetType(tweetId).equals(TweetType.Retweet.name())) { 166 | updateCounter(findParent(tweetId), COL_FAV_COUNT, -1); 167 | } else { 168 | updateCounter(tweetId, COL_FAV_COUNT, -1); 169 | } 170 | } 171 | 172 | public synchronized void incrementLikes(long tweetId) { 173 | if (findTweetType(tweetId).equals(TweetType.Retweet.name())) { 174 | updateCounter(findParent(tweetId), COL_RETWEET_COUNT, 1); 175 | } else { 176 | updateCounter(tweetId, COL_FAV_COUNT, 1); 177 | } 178 | } 179 | 180 | public synchronized boolean incrementRetweets(Retweet retweet) { 181 | return updateCounter(retweet.getRetweeted().getTweetId(), COL_RETWEET_COUNT, 1); 182 | } 183 | 184 | public synchronized boolean incrementMentions(Mention mention) { 185 | return updateCounter(mention.getMentionedTo().getTweetId(), COL_MENTION_COUNT,1); 186 | } 187 | 188 | 189 | private boolean updateCounter(long tweetId, String columnName, int value) { 190 | try { 191 | int affectedRow = queryRunner.update(conn, 192 | "UPDATE " + TABLE_NAME + " SET " + 193 | columnName + " = " + columnName + " + (?) WHERE " + COL_TWEET_ID + " = (?)", 194 | value, 195 | tweetId); 196 | return affectedRow == 1; 197 | } catch (SQLException e) { 198 | System.out.println(e.getMessage()); 199 | return false; 200 | } 201 | } 202 | public synchronized List selectTimelineTweets(int userId, int MAX_COUNT) { 203 | try(PreparedStatement pStmt = conn.prepareStatement( 204 | "SELECT " 205 | + Users.TABLE_NAME + "." + Users.COL_USERID + ", " 206 | + Users.TABLE_NAME + "." + Users.COL_DISPLAY_NAME + ", " 207 | + Users.TABLE_NAME + "." + Users.COL_USERNAME + ", " 208 | + TABLE_NAME + "." + COL_TWEET_ID + ", " 209 | + TABLE_NAME + "." + COL_TEXT + ", " 210 | + TABLE_NAME + "." + COL_FAV_COUNT + ", " 211 | + TABLE_NAME + "." + COL_RETWEET_COUNT + ", " 212 | + TABLE_NAME + "." + COL_MENTION_COUNT + ", " 213 | + TABLE_NAME + "." + COL_SENT_AT 214 | + " FROM " + TABLE_NAME 215 | + " INNER JOIN " + Followers.TABLE_NAME 216 | + " ON " + Followers.TABLE_NAME + "." + Followers.COL_FOLLOWED 217 | + " = " + TABLE_NAME + "." + COL_SENDER_ID 218 | + " AND " + Followers.TABLE_NAME + "." + Followers.COL_FOLLOWER + " = (?)" 219 | + " INNER JOIN " + Users.TABLE_NAME 220 | + " ON " + TABLE_NAME + "." + COL_SENDER_ID + " = " + Users.TABLE_NAME + "." + Users.COL_USERID 221 | + " WHERE " + TABLE_NAME + "." + COL_TWEET_TYPE + " = '" + TweetType.Tweet.name() + "'" 222 | + " ORDER BY " +COL_SENT_AT+ " LIMIT " + MAX_COUNT)) { 223 | pStmt.setInt(1, userId); 224 | 225 | ResultSet resultSet = pStmt.executeQuery(); 226 | return DBUtils.timelineTweetsHandler(DBUtils.resultSetToList(resultSet)); 227 | 228 | } catch (SQLException e ) { 229 | System.out.println(e.getMessage()); 230 | } 231 | 232 | return null; 233 | } 234 | 235 | public synchronized List selectTimelineRetweets(int userId, int MAX_COUNT) { 236 | final String USERS_TABLE_ALIAS_RT = "retweeter"; 237 | final String USERS_TABLE_ALIAS_OS = "original_sender"; 238 | final String TWEETS_TABLE_ALIAS_OG = "original"; 239 | final String TWEETS_TABLE_ALIAS_RT = "retweeted"; 240 | 241 | try(PreparedStatement pStmt = conn.prepareStatement( 242 | "SELECT " 243 | + USERS_TABLE_ALIAS_RT + "." + Users.COL_USERID + " AS retweeter_user_id, " 244 | + USERS_TABLE_ALIAS_RT + "." + Users.COL_DISPLAY_NAME + " AS retweeter_display_name, " 245 | + USERS_TABLE_ALIAS_OS + "." + Users.COL_USERID + ", " 246 | + USERS_TABLE_ALIAS_OS + "." + Users.COL_DISPLAY_NAME + ", " 247 | + USERS_TABLE_ALIAS_OS + "." + Users.COL_USERNAME + ", " 248 | + TWEETS_TABLE_ALIAS_RT + "." + COL_SENDER_ID + ", " 249 | + TWEETS_TABLE_ALIAS_RT + "." + COL_SENT_AT + "," 250 | + TWEETS_TABLE_ALIAS_OG + "." + COL_TWEET_ID + ", " 251 | + TWEETS_TABLE_ALIAS_OG + "." + COL_TEXT + ", " 252 | + TWEETS_TABLE_ALIAS_OG + "." + COL_FAV_COUNT + ", " 253 | + TWEETS_TABLE_ALIAS_OG + "." + COL_RETWEET_COUNT + ", " 254 | + TWEETS_TABLE_ALIAS_OG + "." + COL_MENTION_COUNT + ", " 255 | + TWEETS_TABLE_ALIAS_OG + "." + COL_SENT_AT 256 | + " FROM " + TABLE_NAME + " " + TWEETS_TABLE_ALIAS_RT 257 | + " INNER JOIN " + Users.TABLE_NAME + " AS " + USERS_TABLE_ALIAS_RT + " ON " 258 | + TWEETS_TABLE_ALIAS_RT +"."+COL_SENDER_ID + "=" + USERS_TABLE_ALIAS_RT+"."+Users.COL_USERID 259 | + " INNER JOIN " + Followers.TABLE_NAME + " ON " 260 | + Followers.TABLE_NAME +"."+Followers.COL_FOLLOWED +"="+ TWEETS_TABLE_ALIAS_RT+"."+COL_SENDER_ID 261 | + " INNER JOIN " + TABLE_NAME + " AS " + TWEETS_TABLE_ALIAS_OG + " ON " 262 | + TWEETS_TABLE_ALIAS_RT+"."+COL_PARENT_TWEET +"="+ TWEETS_TABLE_ALIAS_OG+"."+COL_TWEET_ID +" AND "+ TWEETS_TABLE_ALIAS_RT+"."+COL_TWEET_TYPE +"= 'Retweet'" 263 | + " INNER JOIN " + Users.TABLE_NAME + " AS " + USERS_TABLE_ALIAS_OS + " ON " 264 | + TWEETS_TABLE_ALIAS_OG+"."+COL_SENDER_ID +"="+ USERS_TABLE_ALIAS_OS+"."+Users.COL_USERID 265 | + " WHERE " + Followers.TABLE_NAME+"."+Followers.COL_FOLLOWER +"= (?)" + 266 | " ORDER BY " + TWEETS_TABLE_ALIAS_RT + "." + COL_SENT_AT + " LIMIT " + MAX_COUNT)) { 267 | 268 | pStmt.setInt(1, userId); 269 | 270 | ResultSet resultSet = pStmt.executeQuery(); 271 | return DBUtils.timelineRetweetsHandler(DBUtils.resultSetToList(resultSet)); 272 | 273 | } catch (SQLException e ) { 274 | System.out.println(e.getMessage()); 275 | } 276 | 277 | return null; 278 | } 279 | 280 | 281 | public synchronized List selectTimelineQuotes(int userId, int MAX_COUNT) { 282 | try (PreparedStatement pStmt = conn.prepareStatement( 283 | mentionQuoteHelper( 284 | QUOTER_ALIAS, 285 | QUOTED_ALIAS, 286 | TweetType.Quote, 287 | MAX_COUNT 288 | ) 289 | )) { 290 | 291 | pStmt.setInt(1, userId); 292 | 293 | ResultSet resultSet = pStmt.executeQuery(); 294 | return DBUtils.timelineQuoteHandler(DBUtils.resultSetToList(resultSet)); 295 | 296 | } catch (SQLException e) { 297 | System.out.println(e.getMessage()); 298 | } 299 | 300 | return null; 301 | } 302 | 303 | 304 | public synchronized List selectTimelineMentions(int userId, int MAX_COUNT) { 305 | try (PreparedStatement pStmt = conn.prepareStatement( 306 | mentionQuoteHelper( 307 | MENTIONER_ALIAS, 308 | MENTIONED_ALIAS, 309 | TweetType.Mention, 310 | MAX_COUNT 311 | ) 312 | )) { 313 | 314 | pStmt.setInt(1, userId); 315 | 316 | ResultSet resultSet = pStmt.executeQuery(); 317 | return DBUtils.timelineMentionHandler(DBUtils.resultSetToList(resultSet)); 318 | 319 | } catch (SQLException e ) { 320 | System.out.println(e.getMessage()); 321 | } 322 | 323 | return null; 324 | } 325 | 326 | 327 | public synchronized List selectTimelineFavStars() { 328 | try(PreparedStatement pStmt = conn.prepareStatement( 329 | "SELECT " 330 | + Users.TABLE_NAME + "." + Users.COL_USERID + ", " 331 | + Users.TABLE_NAME + "." + Users.COL_DISPLAY_NAME + ", " 332 | + Users.TABLE_NAME + "." + Users.COL_USERNAME + ", " 333 | + TABLE_NAME + "." + COL_TWEET_ID + ", " 334 | + TABLE_NAME + "." + COL_TEXT + ", " 335 | + TABLE_NAME + "." + COL_FAV_COUNT + ", " 336 | + TABLE_NAME + "." + COL_RETWEET_COUNT + ", " 337 | + TABLE_NAME + "." + COL_MENTION_COUNT + ", " 338 | + TABLE_NAME + "." + COL_SENT_AT + 339 | " FROM " + TABLE_NAME + 340 | " JOIN " + Users.TABLE_NAME + " ON " + 341 | TABLE_NAME + "." + COL_SENDER_ID + "=" + Users.TABLE_NAME + "." + Users.COL_USERID + 342 | " WHERE " + 343 | TABLE_NAME + "." + COL_TWEET_TYPE + "= 'Tweet' AND" + " + "+ TABLE_NAME + "." + COL_FAV_COUNT + ">=" + "(?)")) { 344 | 345 | pStmt.setInt(1, FAV_STAR_MIN); 346 | ResultSet resultSet = pStmt.executeQuery(); 347 | 348 | return DBUtils.timelineTweetsHandler(DBUtils.resultSetToList(resultSet)); 349 | 350 | } catch (SQLException e) { 351 | System.out.println(e.getMessage()); 352 | } 353 | return null; 354 | } 355 | 356 | 357 | 358 | private String mentionQuoteHelper( 359 | String users_alias_original, 360 | String tweets_alias_secondary, 361 | TweetType type, 362 | int MAX_COUNT 363 | ) { 364 | String person = (type == TweetType.Quote) ? ("quoter") : ("mentioner"); 365 | 366 | return "SELECT " + 367 | users_alias_original + "." + Users.COL_USERID + " AS original_user_id, " + 368 | users_alias_original + "." + Users.COL_DISPLAY_NAME + " AS original_display_name, " + 369 | users_alias_original + "." + Users.COL_USERNAME + " AS original_username, " + 370 | "original_sender" + "." + Users.COL_USERID + " AS " + person + "_user_id, " + 371 | "original_sender" + "." + Users.COL_DISPLAY_NAME + " AS " + person + "_display_name, " + 372 | "original_sender" + "." + Users.COL_USERNAME + " AS " + person + "_username, " + 373 | "original" + "." + COL_TWEET_ID + " AS original_tweet_id, " + 374 | "original" + "." + COL_TEXT + " AS original_text, " + 375 | "original" + "." + COL_FAV_COUNT + " AS original_fav_count, " + 376 | "original" + "." + COL_RETWEET_COUNT + " AS original_retweet_count, " + 377 | "original" + "." + COL_MENTION_COUNT + " AS original_mention_count, " + 378 | "original" + "." + COL_SENT_AT + " AS original_sent_at, " + 379 | tweets_alias_secondary + "." + COL_TWEET_ID + " AS " + person + "_tweet_id, " + 380 | tweets_alias_secondary + "." + COL_TEXT + " AS " + person + "_text, " + 381 | tweets_alias_secondary + "." + COL_FAV_COUNT + " AS " + person + "_fav_count, " + 382 | tweets_alias_secondary + "." + COL_RETWEET_COUNT + " AS " + person + "_retweet_count, " + 383 | tweets_alias_secondary + "." + COL_MENTION_COUNT + " AS " + person + "_mention_count, " + 384 | tweets_alias_secondary + "." + COL_SENT_AT + " AS " + person + "_sent_at" 385 | + " FROM " + Tweets.TABLE_NAME +" "+ tweets_alias_secondary 386 | + " INNER JOIN " + Users.TABLE_NAME + " as " + "original_sender" +" ON " 387 | + tweets_alias_secondary+"."+COL_SENDER_ID +"="+ "original_sender" +"."+Users.COL_USERID 388 | + " INNER JOIN " + Followers.TABLE_NAME +" on " 389 | + Followers.TABLE_NAME+"."+Followers.COL_FOLLOWED +"="+ tweets_alias_secondary+"."+COL_SENDER_ID 390 | + " inner JOIN " + Tweets.TABLE_NAME + " as " + "original" + " on " 391 | + tweets_alias_secondary+"."+COL_PARENT_TWEET +"="+ "original" +"."+COL_TWEET_ID +" and "+ tweets_alias_secondary+"."+COL_TWEET_TYPE +"=" + "'"+type.name()+"'" 392 | + " INNER JOIN " + Users.TABLE_NAME + " as " + users_alias_original +" on " 393 | + "original" +"."+COL_SENDER_ID +"="+ users_alias_original+"."+Users.COL_USERID 394 | + " WHERE " + Followers.TABLE_NAME+"."+Followers.COL_FOLLOWER +"= (?)" 395 | + "ORDER BY " + tweets_alias_secondary+"."+COL_SENT_AT + " LIMIT " + MAX_COUNT; 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/main/java/Server/ServerRequestHandler.java: -------------------------------------------------------------------------------- 1 | package Server; 2 | 3 | import Server.Controllers.UserActionsController; 4 | import Server.Controllers.DataController; 5 | import Server.Database.DatabaseController; 6 | import Server.Database.MessageData.Tweets; 7 | import Server.Database.UserData.BlockList; 8 | import Server.Database.UserData.Followers; 9 | import Server.Database.UserData.Users; 10 | import Server.Utils.Http.ServerHttpUtils; 11 | import com.twitter.common.Annotations.APIEndpoint; 12 | import com.twitter.common.Models.Messages.Visuals.Image; 13 | import com.twitter.common.Models.Timeline; 14 | import com.twitter.common.Models.UserGraph; 15 | import com.twitter.common.Utils.JwtUtils; 16 | import com.sun.net.httpserver.Headers; 17 | import com.sun.net.httpserver.HttpExchange; 18 | import com.sun.net.httpserver.HttpServer; 19 | import com.twitter.common.API.API; 20 | import com.twitter.common.Models.Messages.Textuals.Mention; 21 | import com.twitter.common.Models.Messages.Textuals.Quote; 22 | import com.twitter.common.Models.Messages.Textuals.Retweet; 23 | import com.twitter.common.Models.Messages.Textuals.Tweet; 24 | import com.twitter.common.Models.User; 25 | 26 | 27 | import java.lang.reflect.InvocationTargetException; 28 | import java.lang.reflect.Method; 29 | import java.net.InetSocketAddress; 30 | import java.util.*; 31 | import java.util.concurrent.Executors; 32 | import java.util.function.BiFunction; 33 | import java.util.function.Function; 34 | 35 | import static Server.Utils.Http.ServerHttpUtils.*; 36 | import static com.twitter.common.API.StatusCode.*; 37 | import static com.twitter.common.Utils.SafeCall.safe; 38 | 39 | @SuppressWarnings("unused") // the methods are used but since they're passed indirectly using reflection the ide can not recognize that 40 | public class ServerRequestHandler { 41 | private HttpServer httpServer; 42 | private static final DatabaseController databaseController = DatabaseController.getInstance(); 43 | private static final UserActionsController requestActionHandler = UserActionsController.getInstance(); 44 | private static final DataController dataController = DataController.getInstance(); 45 | public void run() { 46 | //TODO: try to move db initializations out of here! 47 | safe(()->{ 48 | //zero indicates that http server will use default amount for backlog (how many users to handle concurrently) 49 | httpServer = HttpServer.create(new InetSocketAddress(API.SERVER_IP,API.PORT), 0); 50 | httpServer.setExecutor(Executors.newFixedThreadPool(8)); 51 | databaseController.initializeDB(); 52 | System.out.println("database initialized..."); 53 | createContexts(); 54 | System.out.println("api endpoints connected to the server"); 55 | httpServer.start(); 56 | System.out.println("server started working..."); 57 | }); 58 | } 59 | 60 | private void createContexts() { 61 | Method[] methods = ServerRequestHandler.class.getDeclaredMethods(); 62 | 63 | for (Method method: methods) { 64 | if(method.isAnnotationPresent(APIEndpoint.class)) { 65 | APIEndpoint annotation = method.getAnnotation(APIEndpoint.class); 66 | httpServer.createContext( 67 | annotation.endpoint(), 68 | exchange -> 69 | { 70 | try { 71 | method.invoke(this, exchange); 72 | } catch (IllegalAccessException | InvocationTargetException e) { 73 | throw new RuntimeException(e); 74 | } 75 | } 76 | 77 | ); 78 | } 79 | } 80 | } 81 | 82 | private static boolean JwtCheck(HttpExchange exchange, int userId) { 83 | HashMap validClaims = new HashMap<>(2); 84 | Headers header = exchange.getRequestHeaders(); 85 | if (header.get(JwtUtils.name).size() > 1 || !JwtUtils.JwtValidator(header.getFirst(JwtUtils.name), validClaims)) { 86 | sendErrorResponse(exchange, "you are logged out, please login again", UNAUTHORIZED); 87 | return false; 88 | } 89 | return true; 90 | } 91 | 92 | @APIEndpoint(endpoint = API.SIGN_UP) 93 | private static void signUp(HttpExchange exchange) { 94 | //required http method for sign up is POST 95 | if(ServerHttpUtils.validateMethod("POST", exchange)) { 96 | User toSignUp = validateBody(exchange, User.class); 97 | if(toSignUp == null) { 98 | badRequest(exchange); 99 | } 100 | else if (databaseController.emailExists(toSignUp.getEmail())) { 101 | sendErrorResponse(exchange, "email already in use.", DUPLICATE_RECORD); 102 | 103 | } else if (databaseController.usernameExists(toSignUp.getUsername())) { 104 | sendErrorResponse(exchange, "username already in use.", DUPLICATE_RECORD); 105 | 106 | } else if (toSignUp.getDateOfBirth().after(User.getLegalAge())) { 107 | sendErrorResponse(exchange, "sorry you are not 18 yet.", NOT_ALLOWED); 108 | 109 | } else if (requestActionHandler.signUp(toSignUp)) { 110 | sendSuccessResponse(exchange, "Sign up successfully done.", null); 111 | } 112 | } 113 | } 114 | 115 | @APIEndpoint(endpoint = API.SIGN_IN) 116 | private static void signIn(HttpExchange exchange) { 117 | //required http method for sign-in is GET since after validation 118 | // of credentials all user data will be sent back to the client 119 | if(validateMethod("GET", exchange)) { 120 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 121 | if(validateEssentialKeys(exchange, query, Users.COL_USERNAME, Users.COL_PASSWORD_HASH)) { 122 | User user = requestActionHandler.signIn(query.get(Users.COL_USERNAME), query.get(Users.COL_PASSWORD_HASH)); 123 | if (user == null) { 124 | sendErrorResponse(exchange, "username or password incorrect", UNAUTHORIZED); 125 | } else { 126 | //sends Jwt back to the client inside the user object 127 | user.setJwt(JwtUtils.refreshTokenGenerator(user.getUserId())); 128 | serializableResponse( 129 | exchange, 130 | "sign in successful", 131 | user, 132 | SUCCESS, 133 | true); 134 | } 135 | } 136 | } 137 | } 138 | @APIEndpoint(endpoint = API.FOLLOW) 139 | @SuppressWarnings("unchecked") 140 | private static void follow(HttpExchange exchange) { 141 | if(validateMethod("POST", exchange)) { 142 | Map body = ServerHttpUtils.validateBody(exchange, HashMap.class); 143 | 144 | if(validateEssentialKeys(exchange, body, Followers.COL_FOLLOWED, Followers.COL_FOLLOWER)) { 145 | int followerId = Integer.parseInt(body.get(Followers.COL_FOLLOWER)); 146 | int followedId = Integer.parseInt(body.get(Followers.COL_FOLLOWED)); 147 | 148 | if (JwtCheck(exchange, followerId)) { 149 | if (followedId == followerId) { 150 | sendErrorResponse(exchange, "following user failed", NOT_ALLOWED); 151 | 152 | } else if (requestActionHandler.follow(followerId, followedId)) { 153 | sendSuccessResponse(exchange, "user followed successfully", null); 154 | 155 | } else { 156 | sendErrorResponse(exchange, "you are already following this user", DUPLICATE_RECORD); 157 | } 158 | } 159 | } 160 | } 161 | } 162 | @APIEndpoint(endpoint = API.UNFOLLOW) 163 | @SuppressWarnings("unchecked") 164 | private static void unfollow(HttpExchange exchange) { 165 | if(validateMethod("POST", exchange)) { 166 | Map query = ServerHttpUtils.validateBody(exchange, HashMap.class); 167 | 168 | if(validateEssentialKeys(exchange, query, Followers.COL_FOLLOWED, Followers.COL_FOLLOWER)) { 169 | int followerId = Integer.parseInt(query.get(Followers.COL_FOLLOWER)); 170 | int followedId = Integer.parseInt(query.get(Followers.COL_FOLLOWED)); 171 | 172 | if (JwtCheck(exchange, followerId)) { 173 | if (followedId == followerId) { 174 | sendErrorResponse(exchange, "you can't follow or unfollow yourself", NOT_ALLOWED); 175 | 176 | } else if (requestActionHandler.unfollow(followerId, followedId)) { 177 | sendSuccessResponse(exchange, "user unfollowed successfully", null); 178 | 179 | } else { 180 | sendErrorResponse(exchange, "you do not follow this user", NOT_FOUND); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | @APIEndpoint(endpoint = API.TWEET) 188 | private static void tweet(HttpExchange exchange) { 189 | if(ServerHttpUtils.validateMethod("POST", exchange)) { 190 | Tweet toBeTweeted = validateSerializedBody(exchange, Tweet.class); 191 | if(toBeTweeted == null || toBeTweeted.getSender() == null) { 192 | badRequest(exchange); 193 | } 194 | if (JwtCheck(exchange, toBeTweeted.getSender().getUserId())) { 195 | if (requestActionHandler.tweet(toBeTweeted)) { 196 | sendSuccessResponse( 197 | exchange, 198 | "Tweet successfully sent.", 199 | null); 200 | } 201 | else { 202 | internalServerError(exchange); 203 | } 204 | } 205 | } 206 | } 207 | 208 | @APIEndpoint(endpoint = API.QUOTE) 209 | private static void quote(HttpExchange exchange) { 210 | if(validateMethod("POST", exchange)) { 211 | Quote quote = validateSerializedBody(exchange, Quote.class); 212 | if (JwtCheck(exchange, quote.getSender().getUserId())) { 213 | if(requestActionHandler.quote(quote)) { 214 | sendSuccessResponse( 215 | exchange, 216 | "tweet successfully quoted", 217 | null); 218 | } else { 219 | internalServerError(exchange); 220 | } 221 | } 222 | } 223 | } 224 | 225 | @APIEndpoint(endpoint = API.RETWEET) 226 | private static void retweet(HttpExchange exchange) { 227 | if(ServerHttpUtils.validateMethod("POST", exchange)) 228 | { 229 | Retweet toBeReTweeted = validateSerializedBody(exchange, Retweet.class); 230 | if(toBeReTweeted == null) { 231 | badRequest(exchange); 232 | } 233 | if (JwtCheck(exchange, toBeReTweeted.getSender().getUserId())) { 234 | if (requestActionHandler.retweet(toBeReTweeted)) { 235 | sendSuccessResponse( 236 | exchange, 237 | "retweeted successfully", 238 | null); 239 | } else { 240 | internalServerError(exchange); 241 | } 242 | } 243 | } 244 | } 245 | @APIEndpoint(endpoint = API.MENTION) 246 | private static void mention(HttpExchange exchange) { 247 | if(validateMethod("POST", exchange)) { 248 | Mention mention = validateSerializedBody(exchange, Mention.class); 249 | if (JwtCheck(exchange, mention.getSender().getUserId())) { 250 | if(requestActionHandler.mention(mention)) { 251 | sendSuccessResponse( 252 | exchange, 253 | "tweet mentioned successfully", 254 | null); 255 | } else { 256 | internalServerError(exchange); 257 | } 258 | } 259 | } 260 | } 261 | 262 | @APIEndpoint(endpoint = API.LIKE) 263 | @SuppressWarnings("unchecked") 264 | private static void like(HttpExchange exchange) { 265 | if(validateMethod("POST", exchange)) { 266 | Map query = validateBody(exchange, Map.class); 267 | 268 | if(validateEssentialKeys(exchange, query, Users.COL_USERID, Tweets.COL_TWEET_ID)) { 269 | int likerId = Integer.parseInt(query.get(Users.COL_USERID)); 270 | long tweetId = Long.parseLong(query.get(Tweets.COL_TWEET_ID)); 271 | 272 | if (JwtCheck(exchange, likerId)) { 273 | if (requestActionHandler.like(likerId, tweetId)) { 274 | sendSuccessResponse(exchange, "liked successfully", null); 275 | } else { 276 | internalServerError(exchange); 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | @APIEndpoint(endpoint = API.UNLIKE) 284 | @SuppressWarnings("unchecked") 285 | private static void unlike(HttpExchange exchange) { 286 | if(validateMethod("POST", exchange)) { 287 | Map query = validateBody(exchange, Map.class); 288 | 289 | if(validateEssentialKeys(exchange, query, Users.COL_USERID, Tweets.COL_TWEET_ID)) { 290 | int unLikerId = Integer.parseInt(query.get(Users.COL_USERID)); 291 | long tweetId = Long.parseLong(query.get(Tweets.COL_TWEET_ID)); 292 | 293 | if (JwtCheck(exchange, unLikerId)) { 294 | if (requestActionHandler.unlike(unLikerId, tweetId)) { 295 | sendSuccessResponse(exchange, "unliked successfully", null); 296 | } else { 297 | internalServerError(exchange); 298 | } 299 | } 300 | } 301 | } 302 | } 303 | 304 | @APIEndpoint(endpoint = API.BLOCK) 305 | @SuppressWarnings("unchecked") 306 | private static void block(HttpExchange exchange) { 307 | if(validateMethod("POST", exchange)) { 308 | Map params = validateBody(exchange, HashMap.class); 309 | 310 | if(validateEssentialKeys(exchange, params, BlockList.COL_BLOCKED, BlockList.COL_BLOCKED)) { 311 | int blockerId = Integer.parseInt(params.get(BlockList.COL_BLOCKER)); 312 | int blockedId = Integer.parseInt(params.get(BlockList.COL_BLOCKED)); 313 | 314 | if (JwtCheck(exchange, blockerId)) { 315 | if (blockedId == blockerId) { 316 | sendErrorResponse(exchange, "you can't block/unblock yourself", NOT_ALLOWED); 317 | 318 | } else if (requestActionHandler.block(blockerId, blockedId)) { 319 | sendSuccessResponse(exchange, "user successfully blocked", null); 320 | 321 | } else { 322 | sendErrorResponse(exchange, "you have already blocked this user", DUPLICATE_RECORD); 323 | } 324 | 325 | } 326 | } 327 | } 328 | } 329 | 330 | @APIEndpoint(endpoint = API.UNBLOCK) 331 | @SuppressWarnings("unchecked") 332 | private static void unblock(HttpExchange exchange) { 333 | if(validateMethod("POST", exchange)) { 334 | Map query = validateBody(exchange, HashMap.class); 335 | 336 | if(validateEssentialKeys(exchange, query, BlockList.COL_BLOCKED, BlockList.COL_BLOCKED)) { 337 | int blockerId = Integer.parseInt(query.get(BlockList.COL_BLOCKER)); 338 | int blockedId = Integer.parseInt(query.get(BlockList.COL_BLOCKED)); 339 | 340 | if (JwtCheck(exchange, blockerId)) { 341 | if (blockedId == blockerId) { 342 | sendErrorResponse(exchange, "you can't block/unblock yourself", NOT_ALLOWED); 343 | 344 | } else if (requestActionHandler.unblock(blockerId, blockedId)) { 345 | sendSuccessResponse(exchange, "user successfully unblocked", null); 346 | 347 | } else { 348 | sendErrorResponse(exchange, "you have not blocked this person", NOT_FOUND); 349 | } 350 | 351 | } 352 | } 353 | } 354 | } 355 | 356 | @APIEndpoint(endpoint = API.SET_PROFILE_PIC) 357 | private static void setProfilePicture(HttpExchange exchange) { 358 | setUserVisualAttribute(exchange, "Profile", requestActionHandler::setProfile); 359 | } 360 | 361 | @APIEndpoint(endpoint = API.SET_HEADER) 362 | private static void setHeader(HttpExchange exchange) { 363 | setUserVisualAttribute(exchange, "Header", requestActionHandler::setHeader); 364 | } 365 | 366 | 367 | @APIEndpoint(endpoint = API.SET_DISPLAY_NAME) 368 | private static void setDisplayName(HttpExchange exchange) { 369 | setUserTextualAttribute(exchange, Users.COL_DISPLAY_NAME, requestActionHandler::setDisplayName); 370 | } 371 | 372 | @APIEndpoint(endpoint = API.SET_BIO) 373 | private static void setBio(HttpExchange exchange) { 374 | setUserTextualAttribute(exchange, Users.COL_BIO, requestActionHandler::setBio); 375 | } 376 | 377 | @APIEndpoint(endpoint = API.SET_LOCATION) 378 | private static void setLocation(HttpExchange exchange) { 379 | setUserTextualAttribute(exchange, Users.COL_LOCATION, requestActionHandler::setLocation); 380 | } 381 | 382 | @APIEndpoint(endpoint = API.SET_USERNAME) 383 | private static void setUsername(HttpExchange exchange) { 384 | //TODO: to be implemented 385 | } 386 | 387 | @APIEndpoint(endpoint = API.GET_TIMELINE) 388 | private static void getTimeLine(HttpExchange exchange) { 389 | if(validateMethod("GET", exchange)) { 390 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 391 | int userId = Integer.parseInt(query.get(Users.COL_USERID)); 392 | int max = Integer.parseInt(query.get("max")); //TODO: don't use hardcoded text here 393 | if(max == 0 || userId == 0) { 394 | badRequest(exchange); 395 | } 396 | if(JwtCheck(exchange, userId)) { 397 | Timeline timeline = dataController.getTimeline(userId, max); 398 | serializableResponse( 399 | exchange, 400 | "get timeline success", 401 | timeline, 402 | SUCCESS, 403 | true); 404 | } 405 | } 406 | } 407 | 408 | @APIEndpoint(endpoint = API.CHECK_EMAIL) 409 | private static void emailExists(HttpExchange exchange) { 410 | duplicateCheck(exchange, Users.COL_EMAIL); 411 | } 412 | 413 | @APIEndpoint(endpoint = API.CHECK_USERNAME) 414 | private static void usernameExists(HttpExchange exchange) { 415 | duplicateCheck(exchange, Users.COL_USERNAME); 416 | } 417 | 418 | private static void duplicateCheck(HttpExchange exchange, String type) { 419 | if(validateMethod("GET", exchange)) { 420 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 421 | String username = query.get(type); 422 | if(validateEssentialKeys(exchange, query, type)) { 423 | if (type.equals(Users.COL_USERNAME)) { 424 | sendSuccessResponse( 425 | exchange, 426 | "duplicate username", 427 | databaseController.usernameExists(username)); 428 | 429 | } else if (type.equals(Users.COL_EMAIL)) { 430 | sendSuccessResponse( 431 | exchange, 432 | "duplicate email", 433 | databaseController.emailExists(username)); 434 | } 435 | } 436 | } 437 | } 438 | 439 | @APIEndpoint(endpoint = API.GET_PROFILE) 440 | private static void getUserProfile(HttpExchange exchange) { 441 | if(validateMethod("GET", exchange)) { 442 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 443 | if(validateEssentialKeys(exchange, query, Users.COL_USERID)) { 444 | int userId = Integer.parseInt(query.get(Users.COL_USERID)); 445 | User user = dataController.getUser(userId); 446 | 447 | if(user != null) { 448 | serializableResponse( 449 | exchange, 450 | "user found successfully", 451 | user, 452 | SUCCESS, 453 | true); 454 | } 455 | } 456 | } 457 | } 458 | 459 | @APIEndpoint(endpoint = API.GET_FOLLOWERS_COUNT) 460 | private static void getFollowersCount(HttpExchange exchange) { 461 | int count = getCountHelper(exchange, "follower"); 462 | if(count >= 0) sendSuccessResponse(exchange, "followers count retrieved successfully", count); 463 | } 464 | 465 | 466 | @APIEndpoint(endpoint = API.GET_FOLLOWERS) 467 | private static void getFollowers(HttpExchange exchange) { 468 | userListHelper(exchange, "followers", dataController::getFollowers); 469 | } 470 | 471 | 472 | @APIEndpoint(endpoint = API.GET_FOLLOWINGS_COUNT) 473 | private static void getFollowingsCount(HttpExchange exchange) { 474 | int count = getCountHelper(exchange, "following"); 475 | if(count >= 0) sendSuccessResponse(exchange, "followers count retrieved successfully", count); 476 | } 477 | 478 | 479 | @APIEndpoint(endpoint = API.GET_FOLLOWINGS) 480 | private static void getFollowings(HttpExchange exchange) { 481 | userListHelper(exchange, "followings", dataController::getFollowings); 482 | } 483 | 484 | @APIEndpoint(endpoint = API.SEARCH_USERS) 485 | private static void searchForUser(HttpExchange exchange) { 486 | if(validateMethod("GET", exchange)) { 487 | final String SEARCH_TERM_PARAM_NAME = "search_term"; 488 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 489 | 490 | if(validateEssentialKeys(exchange, query, SEARCH_TERM_PARAM_NAME)) { 491 | String searchTerm = query.get(SEARCH_TERM_PARAM_NAME); 492 | List searchResult = dataController.searchForUser(searchTerm); 493 | if(searchResult != null) 494 | serializableResponse( 495 | exchange, 496 | "search done with " + searchResult.size() + "results.", 497 | new UserGraph(searchResult), 498 | SUCCESS, 499 | true); 500 | else 501 | internalServerError(exchange); 502 | } 503 | } 504 | } 505 | 506 | private static int getCountHelper(HttpExchange exchange, String type) { 507 | if(validateMethod("GET", exchange)) { 508 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 509 | if(validateEssentialKeys(exchange, query, Users.COL_USERID)) { 510 | int userId = Integer.parseInt(query.get(Users.COL_USERID)); 511 | 512 | return switch (type) { 513 | case "following" -> dataController.getFollowingsCount(userId); 514 | case "follower" -> dataController.getFollowersCount(userId); 515 | default -> -1; 516 | }; 517 | } 518 | } 519 | return -2; 520 | } 521 | 522 | 523 | private static void userListHelper(HttpExchange exchange, String listName, Function> getListFunction) { 524 | if (validateMethod("GET", exchange)) { 525 | Map query = queryToMap(exchange.getRequestURI().getQuery()); 526 | if (validateEssentialKeys(exchange, query, Users.COL_USERID)) { 527 | int userId = Integer.parseInt(query.get(Users.COL_USERID)); 528 | if (JwtCheck(exchange, userId)) { 529 | List userList = getListFunction.apply(userId); 530 | if (userList != null) { 531 | serializableResponse( 532 | exchange, 533 | listName + " list retrieved successfully", 534 | new UserGraph(userList), 535 | SUCCESS, 536 | true); 537 | } else { 538 | internalServerError(exchange); 539 | } 540 | } 541 | } 542 | } 543 | } 544 | 545 | private static void setUserVisualAttribute(HttpExchange exchange, String imageType, BiFunction setter) { 546 | if(validateMethod("POST", exchange)) { 547 | Image newImage = validateSerializedBody(exchange, Image.class); 548 | int userId = Integer.parseInt(getValueFromHeader(exchange, Users.COL_USERID)); 549 | if (newImage == null || userId == 0) { 550 | badRequest(exchange); 551 | } else if (JwtCheck(exchange, userId)) { 552 | if (setter.apply(userId, newImage)) { 553 | sendSuccessResponse( 554 | exchange, 555 | imageType + " changed successfully", 556 | null); 557 | } else { 558 | internalServerError(exchange); 559 | } 560 | } 561 | } 562 | } 563 | 564 | 565 | private static void setUserTextualAttribute(HttpExchange exchange, String newAttributeName, BiFunction setter) { 566 | if(validateMethod("POST", exchange)) { 567 | int userId = Integer.parseInt(getValueFromHeader(exchange, Users.COL_USERID)); 568 | String newAttribValue = getValueFromHeader(exchange, newAttributeName); 569 | if(userId == 0 || newAttribValue == null) { 570 | badRequest(exchange); 571 | } 572 | if(setter.apply(userId, newAttribValue)) { 573 | sendSuccessResponse( 574 | exchange, 575 | newAttributeName + " changed successfully!", 576 | null); 577 | } else { 578 | internalServerError(exchange); 579 | } 580 | } 581 | } 582 | } 583 | --------------------------------------------------------------------------------