package fr.ippon.tatami.repository.cassandra;
import fr.ippon.tatami.domain.Attachment;
import fr.ippon.tatami.domain.Group;
import fr.ippon.tatami.repository.*;
import fr.ippon.tatami.service.util.DomainUtil;
import me.prettyprint.cassandra.serializers.StringSerializer;
import me.prettyprint.cassandra.service.template.ColumnFamilyResult;
import me.prettyprint.cassandra.service.template.ColumnFamilyTemplate;
import me.prettyprint.cassandra.service.template.ColumnFamilyUpdater;
import me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate;
import me.prettyprint.cassandra.utils.TimeUUIDUtils;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hector.api.mutation.Mutator;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;
import fr.ippon.tatami.config.ColumnFamilyKeys;
import fr.ippon.tatami.domain.status.*;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.validation.*;
import java.util.*;
/**
* Cassandra implementation of the status repository.
* <p/>
* Timeline and Userline have the same structure :
* - Key : login
* - Name : status Id
* - Value : ""
*
* @author Julien Dubois
*/
@Repository
public class CassandraStatusRepository implements StatusRepository {
private final Logger log = LoggerFactory.getLogger(CassandraStatusRepository.class);
private static final String LOGIN = "login";
private static final String TYPE = "type";
private static final String USERNAME = "username";
private static final String DOMAIN = "domain";
private static final String STATUS_DATE = "statusDate";
//Normal status
private static final String STATUS_PRIVATE = "statusPrivate";
private static final String GROUP_ID = "groupId";
private static final String HAS_ATTACHMENTS = "hasAttachments";
private static final String CONTENT = "content";
private static final String DISCUSSION_ID = "discussionId";
private static final String REPLY_TO = "replyTo";
private static final String REPLY_TO_USERNAME = "replyToUsername";
private static final String REMOVED = "removed";
private static final String GEO_LOCALIZATION = "geoLocalization";
//Share, Mention Share & Announcement
private static final String ORIGINAL_STATUS_ID = "originalStatusId";
//Mention Friend
private static final String FOLLOWER_LOGIN = "followerLogin";
//Bean validation
private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private static final Validator validator = factory.getValidator();
//Cassandra Template
ColumnFamilyTemplate<String, String> template;
@Inject
private Keyspace keyspaceOperator;
@Inject
private DiscussionRepository discussionRepository;
@Inject
private SharesRepository sharesRepository;
@Inject
private StatusAttachmentRepository statusAttachmentRepository;
@Inject
private AttachmentRepository attachmentRepository;
@PostConstruct
public void init() {
template =
new ThriftColumnFamilyTemplate<String, String>(
keyspaceOperator,
ColumnFamilyKeys.STATUS_CF,
StringSerializer.get(),
StringSerializer.get());
}
@Override
public Status createStatus(String login,
boolean statusPrivate,
Group group,
Collection<String> attachmentIds,
String content,
String discussionId,
String replyTo,
String replyToUsername,
String geoLocalization)
throws ConstraintViolationException {
Status status = new Status();
status.setLogin(login);
status.setType(StatusType.STATUS);
String username = DomainUtil.getUsernameFromLogin(login);
status.setUsername(username);
String domain = DomainUtil.getDomainFromLogin(login);
status.setDomain(domain);
status.setContent(content);
Set<ConstraintViolation<Status>> constraintViolations = validator.validate(status);
if (!constraintViolations.isEmpty()) {
if (log.isDebugEnabled()) {
for (ConstraintViolation cv : constraintViolations) {
log.debug("Constraint violation: {}", cv.getMessage());
}
}
throw new ConstraintViolationException(new HashSet<ConstraintViolation<?>>(constraintViolations));
}
ColumnFamilyUpdater<String, String> updater = this.createBaseStatus(status);
updater.setString(CONTENT, content);
status.setStatusPrivate(statusPrivate);
updater.setBoolean(STATUS_PRIVATE, statusPrivate);
if (group != null) {
String groupId = group.getGroupId();
status.setGroupId(groupId);
updater.setString(GROUP_ID, groupId);
}
if (attachmentIds != null && attachmentIds.size() > 0) {
status.setHasAttachments(true);
updater.setBoolean(HAS_ATTACHMENTS, true);
}
if (discussionId != null) {
status.setDiscussionId(discussionId);
updater.setString(DISCUSSION_ID, discussionId);
}
if (replyTo != null) {
status.setReplyTo(replyTo);
updater.setString(REPLY_TO, replyTo);
}
if (replyToUsername != null) {
status.setReplyToUsername(replyToUsername);
updater.setString(REPLY_TO_USERNAME, replyToUsername);
}
if(geoLocalization!=null) {
status.setGeoLocalization(geoLocalization);
updater.setString(GEO_LOCALIZATION, geoLocalization);
}
log.debug("Persisting Status : {}", status);
template.update(updater);
return status;
}
@Override
public Share createShare(String login, String originalStatusId) {
Share share = new Share();
share.setLogin(login);
share.setType(StatusType.SHARE);
String username = DomainUtil.getUsernameFromLogin(login);
share.setUsername(username);
String domain = DomainUtil.getDomainFromLogin(login);
share.setDomain(domain);
ColumnFamilyUpdater<String, String> updater = this.createBaseStatus(share);
updater.setString(ORIGINAL_STATUS_ID, originalStatusId);
share.setOriginalStatusId(originalStatusId);
log.debug("Persisting Share : {}", share);
template.update(updater);
return share;
}
@Override
public Announcement createAnnouncement(String login, String originalStatusId) {
Announcement announcement = new Announcement();
announcement.setLogin(login);
announcement.setType(StatusType.ANNOUNCEMENT);
String username = DomainUtil.getUsernameFromLogin(login);
announcement.setUsername(username);
String domain = DomainUtil.getDomainFromLogin(login);
announcement.setDomain(domain);
ColumnFamilyUpdater<String, String> updater = this.createBaseStatus(announcement);
updater.setString(ORIGINAL_STATUS_ID, originalStatusId);
announcement.setOriginalStatusId(originalStatusId);
log.debug("Persisting Announcement : {}", announcement);
template.update(updater);
return announcement;
}
@Override
public MentionFriend createMentionFriend(String login, String followerLogin) {
MentionFriend mentionFriend = new MentionFriend();
mentionFriend.setLogin(login);
mentionFriend.setType(StatusType.MENTION_FRIEND);
String username = DomainUtil.getUsernameFromLogin(login);
mentionFriend.setUsername(username);
String domain = DomainUtil.getDomainFromLogin(login);
mentionFriend.setDomain(domain);
ColumnFamilyUpdater<String, String> updater = this.createBaseStatus(mentionFriend);
updater.setString(FOLLOWER_LOGIN, followerLogin);
log.debug("Persisting MentionFriend : {}", mentionFriend);
template.update(updater);
return mentionFriend;
}
@Override
public MentionShare createMentionShare(String login, String originalStatusId) {
MentionShare mentionShare = new MentionShare();
mentionShare.setLogin(login);
mentionShare.setType(StatusType.MENTION_SHARE);
String username = DomainUtil.getUsernameFromLogin(login);
mentionShare.setUsername(username);
String domain = DomainUtil.getDomainFromLogin(login);
mentionShare.setDomain(domain);
ColumnFamilyUpdater<String, String> updater = this.createBaseStatus(mentionShare);
updater.setString(ORIGINAL_STATUS_ID, originalStatusId);
mentionShare.setOriginalStatusId(originalStatusId);
log.debug("Persisting MentionShare : {}", mentionShare);
template.update(updater);
return mentionShare;
}
private ColumnFamilyUpdater<String, String> createBaseStatus(AbstractStatus abstractStatus) {
// Generate statusId and statusDate for all statuses
String statusId = TimeUUIDUtils.getUniqueTimeUUIDinMillis().toString();
abstractStatus.setStatusId(statusId);
ColumnFamilyUpdater<String, String> updater = template.createUpdater(statusId);
Date statusDate = Calendar.getInstance().getTime();
updater.setDate(STATUS_DATE, statusDate);
abstractStatus.setStatusDate(statusDate);
// Persist common data : login, username, domain, type
String login = abstractStatus.getLogin();
if (login == null) {
throw new IllegalStateException("Login cannot be null for status: " + abstractStatus);
}
updater.setString(LOGIN, login);
String username = abstractStatus.getUsername();
if (username == null) {
throw new IllegalStateException("Username cannot be null for status: " + abstractStatus);
}
updater.setString(USERNAME, username);
String domain = abstractStatus.getDomain();
if (domain == null) {
throw new IllegalStateException("Domain cannot be null for status: " + abstractStatus);
}
updater.setString(DOMAIN, domain);
updater.setString(TYPE, abstractStatus.getType().name());
return updater;
}
@Override
@Cacheable("status-cache")
public AbstractStatus findStatusById(String statusId) {
if (statusId == null || statusId.equals("")) {
return null;
}
if (log.isTraceEnabled()) {
log.trace("Finding status : " + statusId);
}
ColumnFamilyResult<String, String> result = template.queryColumns(statusId);
if (result.hasResults() == false) {
return null; // No status was found
}
AbstractStatus status = null;
String type = result.getString(TYPE);
if (type == null || type.equals(StatusType.STATUS.name())) {
status = findStatus(result, statusId);
} else if (type.equals(StatusType.SHARE.name())) {
status = findShare(result);
} else if (type.equals(StatusType.ANNOUNCEMENT.name())) {
status = findAnnouncement(result);
} else if (type.equals(StatusType.MENTION_FRIEND.name())) {
status = findMentionFriend(result);
} else if (type.equals(StatusType.MENTION_SHARE.name())) {
status = findMentionShare(result);
} else {
throw new IllegalStateException("Status has an unknown type: " + type);
}
if (status == null) { // Status was not found, or was removed
return null;
}
status.setStatusId(statusId);
status.setLogin(result.getString(LOGIN));
status.setUsername(result.getString(USERNAME));
String domain = result.getString(DOMAIN);
if (domain != null) {
status.setDomain(domain);
} else {
throw new IllegalStateException("Status cannot have a null domain: " + status);
}
status.setStatusDate(result.getDate(STATUS_DATE));
Boolean removed = result.getBoolean(REMOVED);
if (removed != null) {
status.setRemoved(removed);
}
return status;
}
private Status findStatus(ColumnFamilyResult<String, String> result, String statusId) {
Status status = new Status();
status.setStatusId(statusId);
status.setType(StatusType.STATUS);
status.setContent(result.getString(CONTENT));
status.setStatusPrivate(result.getBoolean(STATUS_PRIVATE));
status.setGroupId(result.getString(GROUP_ID));
status.setHasAttachments(result.getBoolean(HAS_ATTACHMENTS));
status.setDiscussionId(result.getString(DISCUSSION_ID));
status.setReplyTo(result.getString(REPLY_TO));
status.setReplyToUsername(result.getString(REPLY_TO_USERNAME));
status.setGeoLocalization(result.getString(GEO_LOCALIZATION));
status.setRemoved(result.getBoolean(REMOVED));
if (status.getRemoved() == Boolean.TRUE) {
return null;
}
status.setDetailsAvailable(computeDetailsAvailable(status));
if (status.getHasAttachments() != null && status.getHasAttachments()) {
Collection<String> attachmentIds = statusAttachmentRepository.findAttachmentIds(statusId);
Collection<Attachment> attachments = new ArrayList<Attachment>();
for (String attachmentId : attachmentIds) {
Attachment attachment = attachmentRepository.findAttachmentMetadataById(attachmentId);
if (attachment != null) {
// We copy everything excepted the attachment content, as we do not want it in the status cache
Attachment attachmentCopy = new Attachment();
attachmentCopy.setAttachmentId(attachmentId);
attachmentCopy.setSize(attachment.getSize());
attachmentCopy.setFilename(attachment.getFilename());
attachments.add(attachment);
}
}
status.setAttachments(attachments);
}
return status;
}
private Share findShare(ColumnFamilyResult<String, String> result) {
Share share = new Share();
share.setType(StatusType.SHARE);
share.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID));
return share;
}
private Announcement findAnnouncement(ColumnFamilyResult<String, String> result) {
Announcement announcement = new Announcement();
announcement.setType(StatusType.ANNOUNCEMENT);
announcement.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID));
return announcement;
}
private MentionFriend findMentionFriend(ColumnFamilyResult<String, String> result) {
MentionFriend mentionFriend = new MentionFriend();
mentionFriend.setType(StatusType.MENTION_FRIEND);
mentionFriend.setFollowerLogin(result.getString(FOLLOWER_LOGIN));
return mentionFriend;
}
private MentionShare findMentionShare(ColumnFamilyResult<String, String> result) {
MentionShare mentionShare = new MentionShare();
mentionShare.setType(StatusType.MENTION_SHARE);
mentionShare.setOriginalStatusId(result.getString(ORIGINAL_STATUS_ID));
return mentionShare;
}
@Override
@CacheEvict(value = "status-cache", key = "#status.statusId")
public void removeStatus(AbstractStatus status) {
log.debug("Removing Status : {}", status);
Mutator<String> mutator = HFactory.createMutator(keyspaceOperator, StringSerializer.get());
mutator.addDeletion(status.getStatusId(), ColumnFamilyKeys.STATUS_CF);
mutator.execute();
}
private boolean computeDetailsAvailable(Status status) {
boolean detailsAvailable = false;
if (status.getType().equals(StatusType.STATUS)) {
if (StringUtils.isNotBlank(status.getReplyTo())) {
detailsAvailable = true;
} else if (discussionRepository.hasReply(status.getStatusId())) {
detailsAvailable = true;
} else if (sharesRepository.hasBeenShared(status.getStatusId())) {
detailsAvailable = true;
}
}
return detailsAvailable;
}
}