/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2017 Jeff Nelson, Cinchapi Software Collective
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.cinchapi.concourse.example.mocktwitter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.cinchapi.concourse.time.Time;
/**
*
*
* @author Jeff Nelson
*/
public class MysqlTwitter implements Twitter {
private static Comparator<Long> REVERSE_CHRONOLOGICAL_SORTER = new Comparator<Long>() {
@Override
public int compare(Long o1, Long o2) {
return -1 * o1.compareTo(o2);
}
};
/**
* Return a string that represents the hash for {@code string}.
*
* @param string
* @return the hash
*/
private static String hash(String string) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(string.getBytes());
// props to http://stackoverflow.com/a/5470268/1336833
StringBuilder hex = new StringBuilder();
for (int i = 0; i < hash.length; i++) {
if((0xff & hash[i]) < 0x10) {
hex.append("0" + Integer.toHexString((0xFF & hash[i])));
}
else {
hex.append(Integer.toHexString(0xFF & hash[i]));
}
}
return hex.toString();
}
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); // this should never happen
}
}
/**
* Generates secure random salt values. This is overkill for the demo, but
* good to use for real applications.
*/
private final SecureRandom srand = new SecureRandom();
/**
* The id of the user that is currenty logged in.
*/
private long userid = 0;
private Connection mysql;
{
try {
Class.forName("com.mysql.jdbc.Driver");
String user = "root";
String password = "root";
String url = "jdbc:mysql://localhost:8889/twitter";
mysql = DriverManager.getConnection(url, user, password);
}
catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
mysql = null;
}
}
@Override
public boolean follow(String username) {
try {
long id = getUserId(username);
if(id != userid) {
PreparedStatement stmt = mysql
.prepareStatement("INSERT INTO followers (follower, followed) VALUES (?,?)");
stmt.setLong(1, userid);
stmt.setLong(2, id);
return stmt.executeUpdate() > 0;
}
else {
return false;
}
}
catch (SQLException e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean login(String username, String password) {
try {
if(exists(username)) {
long userid = getUserId(username);
PreparedStatement stmt = mysql
.prepareStatement("SELECT salt, password FROM users WHERE uid = ?");
stmt.setLong(1, userid);
stmt.execute();
ResultSet result = stmt.getResultSet();
result.next();
long salt = result.getLong(1);
String pword = result.getString(2);
password = hash(password + salt);
if(password.equals(pword)) {
this.userid = userid;
return true;
}
}
return false;
}
catch (SQLException e) {
e.printStackTrace();
return false;
}
}
@Override
public Map<Long, String> mentions() {
try {
PreparedStatement stmt = mysql
.prepareStatement("SELECT tid FROM mentions WHERE uid = ?");
stmt.setLong(1, userid);
stmt.execute();
ResultSet result = stmt.getResultSet();
Set<Long> tids = new HashSet<Long>();
while (result.next()) {
tids.add(result.getLong(1));
}
return getTweetInfo(tids);
}
catch (SQLException e) {
e.printStackTrace();
return null;
}
}
@Override
public boolean register(String username, String password) {
try {
if(!exists(username)) {
long id = Time.now();
long salt = srand.nextLong();
PreparedStatement stmt = mysql
.prepareStatement("INSERT INTO users (uid, username, password, salt) VALUES (?,?,?,?)");
stmt.setLong(1, id);
stmt.setString(2, username);
stmt.setString(3, hash(password + salt));
stmt.setLong(4, salt);
return stmt.executeUpdate() > 0;
}
else {
throw new IllegalArgumentException("Username is already taken!");
}
}
catch (SQLException e) {
e.printStackTrace();
return false;
}
}
@Override
public Map<Long, String> timeline() {
try {
Map<Long, String> timeline = new TreeMap<Long, String>(
REVERSE_CHRONOLOGICAL_SORTER);
// Get all of my tweets
PreparedStatement stmt = mysql
.prepareStatement("SELECT tid FROM tweets where author = ?");
stmt.setLong(1, userid);
stmt.execute();
ResultSet result = stmt.getResultSet();
Set<Long> tids = new HashSet<Long>();
while (result.next()) {
tids.add(result.getLong(1));
}
timeline.putAll(getTweetInfo(tids));
// Get all of the tweets for users i am following
stmt = mysql
.prepareStatement("SELECT followed FROM followers WHERE follower = ?");
stmt.setLong(1, userid);
stmt.execute();
result = stmt.getResultSet();
while (result.next()) {
long followed = result.getLong(1);
PreparedStatement stmt2 = mysql
.prepareStatement("SELECT tid FROM tweets where author = ?");
stmt2.setLong(1, followed);
stmt2.execute();
ResultSet result2 = stmt.getResultSet();
Set<Long> tids2 = new HashSet<Long>();
while (result2.next()) {
tids2.add(result2.getLong(1));
}
timeline.putAll(getTweetInfo(tids2));
}
// Get all the tweets where I am mentioned
timeline.putAll(mentions());
return timeline;
}
catch (SQLException e) {
e.printStackTrace();
return null;
}
}
@Override
public void tweet(String message) {
try {
if(message.length() <= 140) {
long tweetId = Time.now();
PreparedStatement stmt = mysql
.prepareStatement("INSERT INTO tweets (tid, author, message, timestamp) VALUES (?,?,?,?)");
stmt.setLong(1, tweetId);
stmt.setLong(2, userid);
stmt.setString(3, message);
stmt.setLong(4, Time.now());
stmt.executeUpdate();
// Parse out mentioned users and link them to the tweet
Pattern pattern = Pattern.compile("[@][\\w]+");
Matcher matcher = pattern.matcher(message);
PreparedStatement stmt2 = mysql
.prepareStatement("INSERT INTO mentions (tid, uid) VALUES (?,?)");
while (matcher.find()) {
String uname = matcher.group().replace("@", "");
long mentioned = getUserId(uname);
stmt2.clearParameters();
stmt2.setLong(1, tweetId);
stmt2.setLong(2, mentioned);
stmt2.execute();
}
}
else {
throw new IllegalArgumentException("Tweet is too long!");
}
}
catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public boolean unfollow(String username) {
try {
long id = getUserId(username);
if(id != userid) {
PreparedStatement stmt = mysql
.prepareStatement("DELETE FROM folowers WHERE follower = ? AND followerd = ?");
stmt.setLong(1, userid);
stmt.setLong(2, id);
return stmt.executeUpdate() > 0;
}
else {
return false;
}
}
catch (SQLException e) {
e.printStackTrace();
return false;
}
}
/**
* Return {@code true} if a user with {@code username} exists.
*
* @param username
* @return {@code true} if {@code username} exists
* @throws SQLException
*/
private boolean exists(String username) throws SQLException {
PreparedStatement stmt = mysql
.prepareStatement("SELECT uid FROM users WHERE username = ?");
stmt.setString(1, username);
stmt.execute();
ResultSet result = stmt.getResultSet();
return result.next();
}
/**
* Return a map from timestamp to tweet that contains information for the
* set of tweet ids specified in {@code tweets}.
*
* @param tweets
* @return the tweet info
*/
private Map<Long, String> getTweetInfo(Set<Long> tweets) {
try {
Map<Long, String> collection = new TreeMap<Long, String>(
REVERSE_CHRONOLOGICAL_SORTER);
PreparedStatement stmt = mysql
.prepareStatement("SELECT username, message, timestamp FROM tweets JOIN users on author = uid WHERE tid = ?");
for (Long tweetId : tweets) {
stmt.clearParameters();
stmt.setLong(1, tweetId);
stmt.execute();
ResultSet result = stmt.getResultSet();
result.next();
String author = result.getString(1);
String message = result.getString(2);
long timestamp = result.getLong(3);
collection.put(timestamp, author + ": " + message);
}
return collection;
}
catch (SQLException e) {
e.printStackTrace();
return null;
}
}
/**
* Return the user id (primary key) for the user with {@code username}.
*
* @param username
* @return the user id for {@code username}
* @throws SQLException
* @throws IllegalArgumentException
*/
private long getUserId(String username) throws SQLException {
if(exists(username)) {
PreparedStatement stmt = mysql
.prepareStatement("SELECT uid FROM users WHERE username = ?");
stmt.setString(1, username);
stmt.execute();
ResultSet result = stmt.getResultSet();
result.next();
return result.getLong(1);
}
else {
throw new IllegalArgumentException("Invalid user");
}
}
}