/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.samples.retwisj.redis;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BulkMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.query.SortQueryBuilder;
import org.springframework.data.redis.hash.DecoratingStringHashMapper;
import org.springframework.data.redis.hash.HashMapper;
import org.springframework.data.redis.hash.JacksonHashMapper;
import org.springframework.data.redis.samples.retwisj.Post;
import org.springframework.data.redis.samples.retwisj.Range;
import org.springframework.data.redis.samples.retwisj.RetwisSecurity;
import org.springframework.data.redis.samples.retwisj.web.WebPost;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.data.redis.support.collections.DefaultRedisMap;
import org.springframework.data.redis.support.collections.DefaultRedisSet;
import org.springframework.data.redis.support.collections.RedisList;
import org.springframework.data.redis.support.collections.RedisMap;
import org.springframework.data.redis.support.collections.RedisSet;
import org.springframework.util.StringUtils;
/**
* Twitter-clone on top of Redis.
*
* @author Costin Leau
*/
@Named
public class RetwisRepository {
private static final Pattern MENTION_REGEX = Pattern.compile("@[\\w]+");
private final StringRedisTemplate template;
private final ValueOperations<String, String> valueOps;
private final RedisAtomicLong postIdCounter;
private final RedisAtomicLong userIdCounter;
// global users
private RedisList<String> users;
// global timeline
private final RedisList<String> timeline;
private final HashMapper<Post, String, String> postMapper = new DecoratingStringHashMapper<Post>(
new JacksonHashMapper<Post>(Post.class));
@Inject
public RetwisRepository(StringRedisTemplate template) {
this.template = template;
valueOps = template.opsForValue();
users = new DefaultRedisList<String>(KeyUtils.users(), template);
timeline = new DefaultRedisList<String>(KeyUtils.timeline(), template);
userIdCounter = new RedisAtomicLong(KeyUtils.globalUid(), template.getConnectionFactory());
postIdCounter = new RedisAtomicLong(KeyUtils.globalPid(), template.getConnectionFactory());
}
public String addUser(String name, String password) {
String uid = String.valueOf(userIdCounter.incrementAndGet());
// save user as hash
// uid -> user
BoundHashOperations<String, String, String> userOps = template.boundHashOps(KeyUtils.uid(uid));
userOps.put("name", name);
userOps.put("pass", password);
valueOps.set(KeyUtils.user(name), uid);
users.addFirst(name);
return addAuth(name);
}
public List<WebPost> getPost(String pid) {
return Collections.singletonList(convertPost(pid, post(pid)));
}
public List<WebPost> getPosts(String uid, Range range) {
return convertPidsToPosts(KeyUtils.posts(uid), range);
}
public List<WebPost> getTimeline(String uid, Range range) {
return convertPidsToPosts(KeyUtils.timeline(uid), range);
}
public Collection<String> getFollowers(String uid) {
return covertUidsToNames(KeyUtils.followers(uid));
}
public Collection<String> getFollowing(String uid) {
return covertUidsToNames(KeyUtils.following(uid));
}
public List<WebPost> getMentions(String uid, Range range) {
return convertPidsToPosts(KeyUtils.mentions(uid), range);
}
public Collection<WebPost> timeline(Range range) {
return convertPidsToPosts(KeyUtils.timeline(), range);
}
public Collection<String> newUsers(Range range) {
return users.range(range.being, range.end);
}
public void post(String username, WebPost post) {
Post p = post.asPost();
String uid = findUid(username);
p.setUid(uid);
String pid = String.valueOf(postIdCounter.incrementAndGet());
String replyName = post.getReplyTo();
if (StringUtils.hasText(replyName)) {
String mentionUid = findUid(replyName);
p.setReplyUid(mentionUid);
// handle mentions below
p.setReplyPid(post.getReplyPid());
}
// add post
post(pid).putAll(postMapper.toHash(p));
// add links
posts(uid).addFirst(pid);
timeline(uid).addFirst(pid);
// update followers
for (String follower : followers(uid)) {
timeline(follower).addFirst(pid);
}
timeline.addFirst(pid);
handleMentions(p, pid, replyName);
}
private void handleMentions(Post post, String pid, String name) {
// find mentions
Collection<String> mentions = findMentions(post.getContent());
for (String mention : mentions) {
String uid = findUid(mention);
if (uid != null) {
mentions(uid).addFirst(pid);
}
}
}
public String findUid(String name) {
return valueOps.get(KeyUtils.user(name));
}
public boolean isUserValid(String name) {
return template.hasKey(KeyUtils.user(name));
}
public boolean isPostValid(String pid) {
return template.hasKey(KeyUtils.post(pid));
}
private String findName(String uid) {
if (!StringUtils.hasText(uid)) {
return "";
}
BoundHashOperations<String, String, String> userOps = template.boundHashOps(KeyUtils.uid(uid));
return userOps.get("name");
}
public boolean auth(String user, String pass) {
// find uid
String uid = findUid(user);
if (StringUtils.hasText(uid)) {
BoundHashOperations<String, String, String> userOps = template.boundHashOps(KeyUtils.uid(uid));
return userOps.get("pass").equals(pass);
}
return false;
}
public String findNameForAuth(String value) {
String uid = valueOps.get(KeyUtils.authKey(value));
return findName(uid);
}
public String addAuth(String name) {
String uid = findUid(name);
// add random auth key relation
String auth = UUID.randomUUID().toString();
valueOps.set(KeyUtils.auth(uid), auth);
valueOps.set(KeyUtils.authKey(auth), uid);
return auth;
}
public void deleteAuth(String user) {
String uid = findUid(user);
String authKey = KeyUtils.auth(uid);
String auth = valueOps.get(authKey);
template.delete(Arrays.asList(authKey, KeyUtils.authKey(auth)));
}
public boolean hasMorePosts(String targetUid, Range range) {
return posts(targetUid).size() > range.end + 1;
}
public boolean hasMoreTimeline(String targetUid, Range range) {
return timeline(targetUid).size() > range.end + 1;
}
public boolean hasMoreTimeline(Range range) {
return timeline.size() > range.end + 1;
}
public boolean isFollowing(String uid, String targetUid) {
return following(uid).contains(targetUid);
}
public void follow(String targetUser) {
String targetUid = findUid(targetUser);
following(RetwisSecurity.getUid()).add(targetUid);
followers(targetUid).add(RetwisSecurity.getUid());
}
public void stopFollowing(String targetUser) {
String targetUid = findUid(targetUser);
following(RetwisSecurity.getUid()).remove(targetUid);
followers(targetUid).remove(RetwisSecurity.getUid());
}
public List<String> alsoFollowed(String uid, String targetUid) {
RedisSet<String> tempSet = following(uid).intersectAndStore(followers(targetUid),
KeyUtils.alsoFollowed(uid, targetUid));
String key = tempSet.getKey();
template.expire(key, 5, TimeUnit.SECONDS);
return covertUidsToNames(key);
}
public List<String> commonFollowers(String uid, String targetUid) {
RedisSet<String> tempSet = following(uid).intersectAndStore(following(targetUid),
KeyUtils.commonFollowers(uid, targetUid));
tempSet.expire(5, TimeUnit.SECONDS);
return covertUidsToNames(tempSet.getKey());
}
// collections mapping the core data structures
private RedisList<String> timeline(String uid) {
return new DefaultRedisList<String>(KeyUtils.timeline(uid), template);
}
private RedisSet<String> following(String uid) {
return new DefaultRedisSet<String>(KeyUtils.following(uid), template);
}
private RedisSet<String> followers(String uid) {
return new DefaultRedisSet<String>(KeyUtils.followers(uid), template);
}
private RedisList<String> mentions(String uid) {
return new DefaultRedisList<String>(KeyUtils.mentions(uid), template);
}
private RedisMap<String, String> post(String pid) {
return new DefaultRedisMap<String, String>(KeyUtils.post(pid), template);
}
private RedisList<String> posts(String uid) {
return new DefaultRedisList<String>(KeyUtils.posts(uid), template);
}
// various util methods
private String replaceReplies(String content) {
Matcher regexMatcher = MENTION_REGEX.matcher(content);
while (regexMatcher.find()) {
String match = regexMatcher.group();
int start = regexMatcher.start();
int stop = regexMatcher.end();
String uName = match.substring(1);
if (isUserValid(uName)) {
content = content.substring(0, start) + "<a href=\"!" + uName + "\">" + match + "</a>"
+ content.substring(stop);
}
}
return content;
}
private List<String> covertUidsToNames(String key) {
return template.sort(SortQueryBuilder.sort(key).noSort().get("uid:*->name").build());
}
private List<WebPost> convertPidsToPosts(String key, Range range) {
String pid = "pid:*->";
final String pidKey = "#";
final String uid = "uid";
final String content = "content";
final String replyPid = "replyPid";
final String replyUid = "replyUid";
final String time = "time";
SortQuery<String> query = SortQueryBuilder.sort(key).noSort().get(pidKey).get(pid + uid).get(pid + content).get(
pid + replyPid).get(pid + replyUid).get(pid + time).limit(range.being, range.end).build();
BulkMapper<WebPost, String> hm = new BulkMapper<WebPost, String>() {
@Override
public WebPost mapBulk(List<String> bulk) {
Map<String, String> map = new LinkedHashMap<String, String>();
Iterator<String> iterator = bulk.iterator();
String pid = iterator.next();
map.put(uid, iterator.next());
map.put(content, iterator.next());
map.put(replyPid, iterator.next());
map.put(replyUid, iterator.next());
map.put(time, iterator.next());
return convertPost(pid, map);
}
};
List<WebPost> sort = template.sort(query, hm);
return sort;
}
private WebPost convertPost(String pid, Map hash) {
Post post = postMapper.fromHash(hash);
WebPost wPost = new WebPost(post);
wPost.setPid(pid);
wPost.setName(findName(post.getUid()));
wPost.setReplyTo(findName(post.getReplyUid()));
wPost.setContent(replaceReplies(post.getContent()));
return wPost;
}
public static Collection<String> findMentions(String content) {
Matcher regexMatcher = MENTION_REGEX.matcher(content);
List<String> mentions = new ArrayList<String>(4);
while (regexMatcher.find()) {
mentions.add(regexMatcher.group().substring(1));
}
return mentions;
}
}