ConcourseTwitter

This is a sample implementation of the TwitterCLI application that is backed by Concourse.

ConcourseTwitter.java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.cinchapi.concourse.Concourse;
import org.cinchapi.concourse.Link;
import org.cinchapi.concourse.thrift.Operator;
import org.cinchapi.concourse.time.Time;

/**
 * This class mimics a subset of the functionality of Twitter, backed by a
 * {@link Concourse} database. This class is only intended to demo some of the
 * functionality in Concourse and show off the simplicity of programming against
 * the schemaless database. DO NOT use this class in a real application.
 * 
 * @author jnelson
 */
public class ConcourseTwitter implements Twitter {

    /**
     * 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
        }
    }

    private static Comparator<Long> REVERSE_CHRONOLOGICAL_SORTER = new Comparator<Long>() {

        @Override
        public int compare(Long o1, Long o2) {
            return -1 * o1.compareTo(o2);
        }

    };

    /**
     * The id of the user that is currenty logged in.
     */
    private long userid = 0;

    /**
     * The connection to Concourse. This assumes that the server is running in
     * the default (localhost:1717) and has the default admin credentials.
     */
    private final Concourse concourse = Concourse.connect();

    /**
     * Generates secure random salt values. This is overkill for the demo, but
     * good to use for real applications.
     */
    private final SecureRandom srand = new SecureRandom();

    @Override
    public boolean follow(String username) {
        long id = getUserId(username);
        if(id != userid) {
            return concourse.link("following", userid, id)
                    && concourse.link("followers", id, userid);
        }
        else {
            return false;
        }
    }

    @Override
    public boolean login(String username, String password) {
        if(exists(username)) {
            long userid = getUserId(username);
            long salt = concourse.get("salt", userid);
            password = hash(password + salt);
            if(password.equals(concourse.get("password", userid))) {
                this.userid = userid;
                return true;
            }
        }
        return false;
    }

    @Override
    public Map<Long, String> mentions() {
        return getTweetInfo(concourse.fetch("mentions", userid));
    }

    @Override
    public boolean register(String username, String password) {
        if(!exists(username)) {
            long id = Time.now();
            concourse.set("username", username, id);
            long salt = srand.nextLong();
            concourse.set("salt", salt, id);
            concourse.set("password", hash(password + salt), id);
            return true;
        }
        else {
            throw new IllegalArgumentException("Username is already taken!");
        }
    }

    @Override
    public Map<Long, String> timeline() {
        Map<Long, String> timeline = new TreeMap<Long, String>(
                REVERSE_CHRONOLOGICAL_SORTER);

        // Get all of my tweets
        timeline.putAll(getTweetInfo(concourse.fetch("tweets", userid)));

        // Get all of the tweets for users i am following
        Set<Object> following = concourse.fetch("following", userid);
        for (Object link : following) {
            long id = ((Link) link).longValue();
            timeline.putAll(getTweetInfo(concourse.fetch("tweets", id)));
        }

        // Get all the tweets where I am mentioned
        timeline.putAll(mentions());

        return timeline;
    }

    @Override
    public void tweet(String message) {
        if(message.length() <= 140) {
            long tweetId = Time.now();
            concourse.set("message", message, tweetId);
            concourse.set("timestamp", Time.now(), tweetId);

            // Link the tweet to the author
            concourse.link("author", tweetId, userid);

            // Link the author to the tweet
            concourse.link("tweets", userid, tweetId);

            // Parse out mentioned users and link them to the tweet
            Pattern pattern = Pattern.compile("[@][\\w]+");
            Matcher matcher = pattern.matcher(message);
            while (matcher.find()) {
                try {
                    String uname = matcher.group().replace("@", "");
                    long mentioned = getUserId(uname);
                    concourse.link("mentions", mentioned, tweetId);
                }
                catch (IllegalArgumentException e) {
                    continue; // uname does not exist
                }
            }
        }
        else {
            throw new IllegalArgumentException("Tweet is too long!");
        }

    }

    @Override
    public boolean unfollow(String username) {
        long id = getUserId(username);
        return concourse.unlink("following", userid, id)
                && concourse.unlink("followers", id, userid);
    }

    /**
     * Return {@code true} if a user with {@code username} exists.
     * 
     * @param username
     * @return {@code true} if {@code username} exists
     */
    private boolean exists(String username) {
        return !concourse.find("username", Operator.EQUALS, username).isEmpty();
    }

    /**
     * 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<Object> tweets) {
        Map<Long, String> collection = new TreeMap<Long, String>(
                REVERSE_CHRONOLOGICAL_SORTER);
        for (Object link : tweets) {
            long id = ((Link) link).longValue();
            long timestamp = concourse.get("timestamp", id);
            String author = concourse.get("username",
                    concourse.<Link> get("author", id).longValue());
            String message = concourse.get("message", id);
            collection.put(timestamp, author + ": " + message);
        }
        return collection;
    }

    /**
     * Return the user id (primary key) for the user with {@code username}.
     * 
     * @param username
     * @return the user id for {@code username}
     * @throws IllegalArgumentException
     */
    private long getUserId(String username) {
        if(exists(username)) {
            return concourse.find("username", Operator.EQUALS, username)
                    .iterator().next();
        }
        else {
            throw new IllegalArgumentException("Invalid user");
        }

    }

}