models.NotificationEvent.java Source code

Java tutorial

Introduction

Here is the source code for models.NotificationEvent.java

Source

/**
 * Yobi, Project Hosting SW
 *
 * Copyright 2013 NAVER Corp.
 * http://yobi.io
 *
 * @Author Yi EungJun
 *
 * 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 models;

import controllers.UserApp;
import controllers.routes;
import models.enumeration.*;
import models.resource.GlobalResource;
import models.resource.Resource;
import models.resource.ResourceConvertible;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.joda.time.DateTime;
import org.tmatesoft.svn.core.SVNException;
import play.api.i18n.Lang;
import play.db.ebean.Model;
import play.i18n.Messages;
import play.libs.Akka;
import playRepository.*;
import scala.concurrent.duration.Duration;
import utils.EventConstants;
import utils.RouteUtil;

import javax.naming.LimitExceededException;
import javax.persistence.*;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static models.enumeration.EventType.*;

@Entity
public class NotificationEvent extends Model {
    private static final long serialVersionUID = 1L;

    @Id
    public Long id;

    public static Finder<Long, NotificationEvent> find = new Finder<>(Long.class, NotificationEvent.class);

    public String title;

    public Long senderId;

    @ManyToMany(cascade = CascadeType.ALL)
    public Set<User> receivers;

    @Temporal(TemporalType.TIMESTAMP)
    public Date created;

    @Enumerated(EnumType.STRING)
    public ResourceType resourceType;

    public String resourceId;

    @Enumerated(EnumType.STRING)
    public EventType eventType;

    @Lob
    public String oldValue;

    @Lob
    public String newValue;

    @OneToOne(mappedBy = "notificationEvent", cascade = CascadeType.ALL)
    public NotificationMail notificationMail;

    public String getOldValue() {
        return oldValue;
    }

    @Transient
    public String getMessage() {
        return getMessage(Lang.defaultLang());
    }

    @Transient
    public String getMessage(Lang lang) {
        switch (eventType) {
        case ISSUE_STATE_CHANGED:
            if (newValue.equals(State.CLOSED.state())) {
                return Messages.get(lang, "notification.issue.closed");
            } else {
                return Messages.get(lang, "notification.issue.reopened");
            }
        case ISSUE_ASSIGNEE_CHANGED:
            if (newValue == null) {
                return Messages.get(lang, "notification.issue.unassigned");
            } else {
                return Messages.get(lang, "notification.issue.assigned", newValue);
            }
        case NEW_ISSUE:
        case NEW_POSTING:
        case NEW_COMMENT:
        case NEW_PULL_REQUEST:
        case NEW_COMMIT:
        case ISSUE_BODY_CHANGED:
            return newValue;
        case NEW_REVIEW_COMMENT:
            try {
                ReviewComment reviewComment = ReviewComment.find.byId(Long.valueOf(this.resourceId));
                if (reviewComment != null) {
                    return buildCommentedCodeMessage(reviewComment, lang);
                }
            } catch (Exception e) {
                play.Logger.error("Failed to generate a notification " + "message for a review comment", e);
            }

            return newValue;
        case PULL_REQUEST_STATE_CHANGED:
            if (State.OPEN.state().equals(newValue)) {
                return Messages.get(lang, "notification.pullrequest.reopened");
            } else {
                return Messages.get(lang, "notification.pullrequest." + newValue);
            }
        case PULL_REQUEST_COMMIT_CHANGED:
            return newValue;
        case PULL_REQUEST_MERGED:
            return Messages.get(lang, "notification.type.pullrequest.merged." + newValue) + "\n"
                    + StringUtils.defaultString(oldValue, StringUtils.EMPTY);
        case MEMBER_ENROLL_REQUEST:
            if (RequestState.REQUEST.name().equals(newValue)) {
                return Messages.get(lang, "notification.member.enroll.request");
            } else if (RequestState.ACCEPT.name().equals(newValue)) {
                return Messages.get(lang, "notification.member.enroll.accept");
            } else {
                return Messages.get(lang, "notification.member.enroll.cancel");
            }
        case ORGANIZATION_MEMBER_ENROLL_REQUEST:
            if (RequestState.REQUEST.name().equals(newValue)) {
                return Messages.get(lang, "notification.organization.member.enroll.request");
            } else if (RequestState.ACCEPT.name().equals(newValue)) {
                return Messages.get(lang, "notification.organization.member.enroll.accept");
            } else {
                return Messages.get(lang, "notification.organization.member.enroll.cancel");
            }
        case PULL_REQUEST_REVIEW_STATE_CHANGED:
            if (PullRequestReviewAction.DONE.name().equals(newValue)) {
                return Messages.get(lang, "notification.pullrequest.reviewed", User.find.byId(senderId).loginId);
            } else {
                return Messages.get(lang, "notification.pullrequest.unreviewed", User.find.byId(senderId).loginId);
            }
        case REVIEW_THREAD_STATE_CHANGED:
            if (newValue.equals(CommentThread.ThreadState.CLOSED.name())) {
                return Messages.get(lang, "notification.reviewthread.closed");
            } else {
                return Messages.get(lang, "notification.reviewthread.reopened");
            }
        default:
            return null;
        }
    }

    /**
     * Builds a notification message for a comment on code.
     *
     * The message contains the commented hunk of the code as below:
     *
     *     In foo.c:
     *
     *     > @@ -1,5 +1,5 @@
     *     >   int bar(void)
     *     >   {
     *     > -     printf("good");
     *     > +     printf("good");
     *
     *     Looks good to me
     *
     *     >       return 0;
     *     >   }
     *
     * Note: This method has a performance issue. See the comment in the method
     * body for the details.
     *
     * @param reviewComment
     * @param lang
     * @return
     * @throws IOException
     */
    private static String buildCommentedCodeMessage(ReviewComment reviewComment, Lang lang) throws IOException {
        if (reviewComment.thread == null || !reviewComment.thread.getFirstReviewComment().equals(reviewComment)
                || !(reviewComment.thread instanceof CodeCommentThread)) {
            return reviewComment.getContents();
        }

        CodeCommentThread thread = (CodeCommentThread) reviewComment.thread;

        PlayRepository repo;

        try {
            repo = RepositoryService.getRepository(thread.project);
        } catch (Exception e) {
            play.Logger.error("Failed to get the repository", e);
            return reviewComment.getContents();
        }

        CodeRange codeRange = thread.codeRange;

        List<FileDiff> diffs;
        if (thread.prevCommitId == null) {
            diffs = repo.getDiff(thread.commitId);
        } else {
            diffs = repo.getDiff(thread.prevCommitId, thread.commitId);
        }

        for (FileDiff diff : diffs) {
            if (!codeRange.isFor(diff))
                continue;

            StringBuilder message = new StringBuilder();

            message.append(Messages.get(lang, "notification.reviewthread.inTheFile", codeRange.path));
            message.append("\n");

            diff.setInterestLine(codeRange.endLine);
            diff.setInterestSide(codeRange.endSide);

            message.append("```diff\n");

            // FIXME: Performance Issue: The hunks of this diffs were
            // already computed but it was not necessary because they will
            // and should be recomputed here.
            for (Hunk hunk : diff.getHunks()) {
                message.append(String.format("> @@ -%d, %d +%d, %d @@\n", hunk.beginA + 1,
                        (hunk.endA - hunk.beginA), hunk.beginB + 1, (hunk.endB - hunk.beginB)));
                for (DiffLine line : hunk.lines) {
                    message.append("> ");
                    switch (line.kind) {
                    case CONTEXT:
                        message.append(" ");
                        break;
                    case ADD:
                        message.append("+");
                        break;
                    case REMOVE:
                        message.append("-");
                        break;
                    }
                    message.append(line.content + "\n");
                    if (codeRange.endsWith(line)) {
                        message.append("```\n");
                        message.append("\n" + reviewComment.getContents() + "\n\n");
                        message.append("```diff\n");
                    }
                }
            }
            message.append("```\n");

            return message.toString();
        }

        return reviewComment.getContents();
    }

    public User getSender() {
        return User.find.byId(this.senderId);
    }

    public Resource getResource() {
        return Resource.get(resourceType, resourceId);
    }

    public Project getProject() {
        switch (resourceType) {
        case ISSUE_ASSIGNEE:
            return Assignee.finder.byId(Long.valueOf(resourceId)).project;
        case PROJECT:
            return Project.find.byId(Long.valueOf(resourceId));
        default:
            Resource resource = getResource();
            if (resource != null) {
                if (resource instanceof GlobalResource) {
                    return null;
                } else {
                    return resource.getProject();
                }
            } else {
                return null;
            }
        }
    }

    public Organization getOrganization() {
        switch (resourceType) {
        case ORGANIZATION:
            return Organization.find.byId(Long.valueOf(resourceId));
        default:
            return null;
        }
    }

    public boolean resourceExists() {
        return Resource.exists(resourceType, resourceId);
    }

    public static void add(NotificationEvent event) {
        if (event.notificationMail == null) {
            event.notificationMail = new NotificationMail();
            event.notificationMail.notificationEvent = event;
        }

        Date draftDate = DateTime.now().minusMillis(EventConstants.DRAFT_TIME_IN_MILLIS).toDate();

        NotificationEvent lastEvent = NotificationEvent.find.where().eq("resourceId", event.resourceId)
                .eq("resourceType", event.resourceType).gt("created", draftDate).orderBy("id desc").setMaxRows(1)
                .findUnique();

        if (lastEvent != null) {
            if (lastEvent.eventType == event.eventType && event.senderId.equals(lastEvent.senderId)) {
                // If the last event is A -> B and the current event is B -> C,
                // they are merged into the new event A -> C.
                event.oldValue = lastEvent.getOldValue();
                lastEvent.delete();

                // If the last event is A -> B and the current event is B -> A,
                // they are removed.
                if (StringUtils.equals(event.oldValue, event.newValue)) {
                    return;
                }
            }
        }

        filterReceivers(event);
        if (event.receivers.isEmpty()) {
            return;
        }
        event.save();
        event.saveManyToManyAssociations("receivers");
    }

    private static void filterReceivers(final NotificationEvent event) {
        final Project project = event.getProject();
        if (project == null) {
            return;
        }

        final Resource resource = project.asResource();
        CollectionUtils.filter(event.receivers, new Predicate() {
            @Override
            public boolean evaluate(Object obj) {
                User receiver = (User) obj;
                if (receiver.loginId == null) {
                    return false;
                }

                if (!Watch.isWatching(receiver, resource)) {
                    return true;
                }
                return UserProjectNotification.isEnabledNotiType(receiver, project, event.eventType);
            }
        });
    }

    public static void deleteBy(Resource resource) {
        for (NotificationEvent event : NotificationEvent.find.where().where().eq("resourceType", resource.getType())
                .eq("resourceId", resource.getId()).findList()) {
            event.delete();
        }
    }

    /**
     * @see {@link controllers.PullRequestApp#newPullRequest(String, String)}
     */
    public static NotificationEvent afterNewPullRequest(User sender, PullRequest pullRequest) {
        NotificationEvent notiEvent = createFrom(sender, pullRequest);
        notiEvent.title = formatNewTitle(pullRequest);
        notiEvent.receivers = getReceiversWithRelatedAuthors(sender, pullRequest);
        notiEvent.eventType = NEW_PULL_REQUEST;
        notiEvent.oldValue = null;
        notiEvent.newValue = pullRequest.body;
        NotificationEvent.add(notiEvent);
        return notiEvent;
    }

    public String getUrlToView() {
        switch (eventType) {
        case MEMBER_ENROLL_REQUEST:
            if (getProject() == null) {
                return null;
            } else {
                return routes.ProjectApp.members(getProject().owner, getProject().name).url();
            }
        case ORGANIZATION_MEMBER_ENROLL_REQUEST:
            Organization organization = getOrganization();
            if (organization == null) {
                return null;
            }
            return routes.OrganizationApp.members(organization.name).url();

        case NEW_COMMIT:
            if (getProject() == null) {
                return null;
            } else {
                return routes.CodeHistoryApp.historyUntilHead(getProject().owner, getProject().name).url();
            }
        default:
            return RouteUtil.getUrl(resourceType, resourceId);
        }
    }

    /**
     * @see {@link models.PullRequest#merge(models.PullRequestEventMessage)}
     * @see {@link controllers.PullRequestApp#addNotification(models.PullRequest, models.enumeration.State, models.enumeration.State)}
     */
    public static NotificationEvent afterPullRequestUpdated(User sender, PullRequest pullRequest, State oldState,
            State newState) {
        NotificationEvent notiEvent = createFrom(sender, pullRequest);
        notiEvent.title = formatReplyTitle(pullRequest);
        notiEvent.receivers = getReceivers(sender, pullRequest);
        notiEvent.eventType = PULL_REQUEST_STATE_CHANGED;
        notiEvent.oldValue = oldState.state();
        notiEvent.newValue = newState.state();
        NotificationEvent.add(notiEvent);
        return notiEvent;
    }

    /**
     * @see {@link actors.PullRequestActor#processPullRequestMerging(models.PullRequestEventMessage, models.PullRequest)}
     */
    public static NotificationEvent afterMerge(User sender, PullRequest pullRequest, State state) {
        NotificationEvent notiEvent = createFrom(sender, pullRequest);
        notiEvent.title = formatReplyTitle(pullRequest);
        notiEvent.receivers = state == State.MERGED ? getReceiversWithRelatedAuthors(sender, pullRequest)
                : getReceivers(sender, pullRequest);
        notiEvent.eventType = PULL_REQUEST_MERGED;
        notiEvent.newValue = state.state();
        NotificationEvent.add(notiEvent);
        return notiEvent;
    }

    /**
     * @see {@link controllers.PullRequestApp#newComment(String, String, Long, String)}
     */
    public static void afterNewComment(User sender, PullRequest pullRequest, ReviewComment newComment,
            String urlToView) {
        NotificationEvent notiEvent = createFrom(sender, newComment);
        notiEvent.title = formatReplyTitle(pullRequest);
        Set<User> receivers = getMentionedUsers(newComment.getContents());
        receivers.addAll(getReceivers(sender, pullRequest));
        receivers.remove(User.findByLoginId(newComment.author.loginId));
        notiEvent.receivers = receivers;
        notiEvent.eventType = NEW_REVIEW_COMMENT;
        notiEvent.oldValue = null;
        notiEvent.newValue = newComment.getContents();

        NotificationEvent.add(notiEvent);
    }

    public static NotificationEvent afterNewPullRequest(PullRequest pullRequest) {
        return afterNewPullRequest(UserApp.currentUser(), pullRequest);
    }

    public static NotificationEvent afterPullRequestUpdated(PullRequest pullRequest, State oldState,
            State newState) {
        return afterPullRequestUpdated(UserApp.currentUser(), pullRequest, oldState, newState);
    }

    public static void afterNewComment(Comment comment) {
        AbstractPosting post = comment.getParent();

        NotificationEvent notiEvent = createFromCurrentUser(comment);
        notiEvent.title = formatReplyTitle(post);
        Set<User> receivers = getReceivers(post);
        receivers.addAll(getMentionedUsers(comment.contents));
        receivers.remove(UserApp.currentUser());
        notiEvent.receivers = receivers;
        notiEvent.eventType = NEW_COMMENT;
        notiEvent.oldValue = null;
        notiEvent.newValue = comment.contents;
        notiEvent.resourceType = comment.asResource().getType();
        notiEvent.resourceId = comment.asResource().getId();

        NotificationEvent.add(notiEvent);
    }

    public static void afterNewCommentWithState(Comment comment, State state) {
        AbstractPosting post = comment.getParent();

        NotificationEvent notiEvent = createFromCurrentUser(comment);
        notiEvent.title = formatReplyTitle(post);
        Set<User> receivers = getReceivers(post);
        receivers.addAll(getMentionedUsers(comment.contents));
        receivers.remove(UserApp.currentUser());
        notiEvent.receivers = receivers;
        notiEvent.eventType = NEW_COMMENT;
        notiEvent.oldValue = null;
        notiEvent.newValue = comment.contents + "\n" + state.state();
        notiEvent.resourceType = comment.asResource().getType();
        notiEvent.resourceId = comment.asResource().getId();

        NotificationEvent.add(notiEvent);
    }

    public static NotificationEvent afterStateChanged(State oldState, Issue issue) {
        NotificationEvent notiEvent = createFromCurrentUser(issue);
        notiEvent.title = formatReplyTitle(issue);
        notiEvent.receivers = getReceivers(issue);
        notiEvent.eventType = ISSUE_STATE_CHANGED;
        notiEvent.oldValue = oldState != null ? oldState.state() : null;
        notiEvent.newValue = issue.state.state();

        NotificationEvent.add(notiEvent);

        return notiEvent;
    }

    public static NotificationEvent afterStateChanged(CommentThread.ThreadState oldState, CommentThread thread)
            throws IOException, SVNException, ServletException {
        NotificationEvent notiEvent = createFromCurrentUser(thread);

        notiEvent.eventType = REVIEW_THREAD_STATE_CHANGED;
        notiEvent.oldValue = oldState.name() != null ? oldState.name() : null;
        notiEvent.newValue = thread.state.name();

        // Set receivers
        Set<User> receivers;
        if (thread.isOnPullRequest()) {
            PullRequest pullRequest = thread.pullRequest;
            notiEvent.title = formatReplyTitle(pullRequest);
            receivers = pullRequest.getWatchers();
        } else {
            String commitId;
            if (thread instanceof CodeCommentThread) {
                commitId = ((CodeCommentThread) thread).commitId;
            } else {
                commitId = ((NonRangedCodeCommentThread) thread).commitId;
            }
            Project project = thread.project;
            Commit commit = RepositoryService.getRepository(project).getCommit(commitId);
            notiEvent.title = formatReplyTitle(project, commit);
            receivers = commit.getWatchers(project);
        }
        receivers.remove(UserApp.currentUser());
        notiEvent.receivers = receivers;

        NotificationEvent.add(notiEvent);

        return notiEvent;
    }

    public static NotificationEvent afterAssigneeChanged(User oldAssignee, Issue issue) {
        NotificationEvent notiEvent = createFromCurrentUser(issue);

        Set<User> receivers = getReceivers(issue);
        if (oldAssignee != null) {
            notiEvent.oldValue = oldAssignee.loginId;
            if (!oldAssignee.loginId.equals(UserApp.currentUser().loginId)) {
                receivers.add(oldAssignee);
            }
        }

        if (issue.assignee != null) {
            notiEvent.newValue = User.find.byId(issue.assignee.user.id).loginId;
        }
        notiEvent.title = formatReplyTitle(issue);
        notiEvent.receivers = receivers;
        notiEvent.eventType = ISSUE_ASSIGNEE_CHANGED;

        NotificationEvent.add(notiEvent);

        return notiEvent;
    }

    public static void afterNewIssue(Issue issue) {
        NotificationEvent notiEvent = createFromCurrentUser(issue);
        notiEvent.title = formatNewTitle(issue);
        notiEvent.receivers = getReceivers(issue);
        notiEvent.eventType = NEW_ISSUE;
        notiEvent.oldValue = null;
        notiEvent.newValue = issue.body;

        NotificationEvent.add(notiEvent);
    }

    public static NotificationEvent afterIssueBodyChanged(String oldBody, Issue issue) {
        NotificationEvent notiEvent = createFromCurrentUser(issue);
        notiEvent.title = formatReplyTitle(issue);
        notiEvent.receivers = getReceivers(issue);
        notiEvent.eventType = EventType.ISSUE_BODY_CHANGED;
        notiEvent.oldValue = oldBody;
        notiEvent.newValue = issue.body;

        NotificationEvent.add(notiEvent);

        return notiEvent;
    }

    public static void afterNewPost(Posting post) {
        NotificationEvent notiEvent = createFromCurrentUser(post);
        notiEvent.title = formatNewTitle(post);
        notiEvent.receivers = getReceivers(post);
        notiEvent.eventType = NEW_POSTING;
        notiEvent.oldValue = null;
        notiEvent.newValue = post.body;

        NotificationEvent.add(notiEvent);
    }

    public static void afterNewCommitComment(Project project, ReviewComment comment, String commitId)
            throws IOException, SVNException, ServletException {
        Commit commit = RepositoryService.getRepository(project).getCommit(commitId);
        Set<User> watchers = commit.getWatchers(project);
        watchers.addAll(getMentionedUsers(comment.getContents()));
        watchers.remove(UserApp.currentUser());

        NotificationEvent notiEvent = createFromCurrentUser(comment);
        notiEvent.title = formatReplyTitle(project, commit);
        notiEvent.receivers = watchers;
        notiEvent.eventType = NEW_REVIEW_COMMENT;
        notiEvent.oldValue = null;
        notiEvent.newValue = comment.getContents();

        NotificationEvent.add(notiEvent);
    }

    public static void afterNewSVNCommitComment(Project project, CommitComment codeComment)
            throws IOException, SVNException, ServletException {
        Commit commit = RepositoryService.getRepository(project).getCommit(codeComment.commitId);
        Set<User> watchers = commit.getWatchers(project);
        watchers.addAll(getMentionedUsers(codeComment.contents));
        watchers.remove(UserApp.currentUser());

        NotificationEvent notiEvent = createFromCurrentUser(codeComment);
        notiEvent.title = formatReplyTitle(project, commit);
        notiEvent.receivers = watchers;
        notiEvent.eventType = NEW_COMMENT;
        notiEvent.oldValue = null;
        notiEvent.newValue = codeComment.contents;

        NotificationEvent.add(notiEvent);
    }

    public static void afterMemberRequest(Project project, User user, RequestState state) {
        NotificationEvent notiEvent = createFromCurrentUser(project);
        notiEvent.eventType = MEMBER_ENROLL_REQUEST;
        notiEvent.receivers = getReceivers(project);
        notiEvent.newValue = state.name();
        if (state == RequestState.ACCEPT || state == RequestState.REJECT) {
            notiEvent.receivers.remove(UserApp.currentUser());
            notiEvent.receivers.add(user);
        }

        switch (state) {
        case REQUEST:
            notiEvent.title = formatMemberRequestTitle(project, user);
            notiEvent.oldValue = RequestState.CANCEL.name();
            break;
        case CANCEL:
            notiEvent.title = formatMemberRequestCancelTitle(project, user);
            notiEvent.oldValue = RequestState.REQUEST.name();
            break;
        case ACCEPT:
            notiEvent.title = formatMemberAcceptTitle(project, user);
            notiEvent.oldValue = RequestState.REQUEST.name();
            break;
        }

        notiEvent.resourceType = project.asResource().getType();
        notiEvent.resourceId = project.asResource().getId();
        NotificationEvent.add(notiEvent);
    }

    public static void afterOrganizationMemberRequest(Organization organization, User user, RequestState state) {
        NotificationEvent notiEvent = createFromCurrentUser(organization);
        notiEvent.eventType = ORGANIZATION_MEMBER_ENROLL_REQUEST;
        notiEvent.receivers = getReceivers(organization);
        notiEvent.newValue = state.name();
        if (state == RequestState.ACCEPT || state == RequestState.REJECT) {
            notiEvent.receivers.remove(UserApp.currentUser());
            notiEvent.receivers.add(user);
        }

        switch (state) {
        case REQUEST:
            notiEvent.title = formatMemberRequestTitle(organization, user);
            notiEvent.oldValue = RequestState.CANCEL.name();
            break;
        case CANCEL:
            notiEvent.title = formatMemberRequestCancelTitle(organization, user);
            notiEvent.oldValue = RequestState.REQUEST.name();
            break;
        case ACCEPT:
            notiEvent.title = formatMemberAcceptTitle(organization, user);
            notiEvent.oldValue = RequestState.REQUEST.name();
            break;
        }

        notiEvent.resourceType = organization.asResource().getType();
        notiEvent.resourceId = organization.asResource().getId();
        NotificationEvent.add(notiEvent);
    }

    public static void afterNewCommits(List<RevCommit> commits, List<String> refNames, Project project, User sender,
            String title, Set<User> watchers) {
        NotificationEvent notiEvent = createFrom(sender, project);
        notiEvent.title = title;
        notiEvent.receivers = watchers;
        notiEvent.eventType = NEW_COMMIT;
        notiEvent.oldValue = null;
        notiEvent.newValue = newCommitsMessage(commits, refNames, project);
        notiEvent.resourceType = project.asResource().getType();
        notiEvent.resourceId = project.asResource().getId();
        NotificationEvent.add(notiEvent);
    }

    public static NotificationEvent afterReviewed(PullRequest pullRequest, PullRequestReviewAction reviewAction) {
        String title = formatReplyTitle(pullRequest);
        Resource resource = pullRequest.asResource();
        Set<User> receivers = pullRequest.getWatchers();
        receivers.add(pullRequest.contributor);
        User reviewer = UserApp.currentUser();
        receivers.remove(reviewer);

        NotificationEvent notiEvent = new NotificationEvent();
        notiEvent.created = new Date();
        notiEvent.title = title;
        notiEvent.senderId = reviewer.id;
        notiEvent.receivers = receivers;
        notiEvent.resourceId = resource.getId();
        notiEvent.resourceType = resource.getType();
        notiEvent.eventType = EventType.PULL_REQUEST_REVIEW_STATE_CHANGED;
        notiEvent.oldValue = reviewAction.getOppositAction().name();
        notiEvent.newValue = reviewAction.name();

        add(notiEvent);

        return notiEvent;
    }

    private static String newCommitsMessage(List<RevCommit> commits, List<String> refNames, Project project) {
        StringBuilder result = new StringBuilder();

        if (commits.size() > 0) {
            result.append("### " + Messages.get("notification.pushed.newcommits") + "\n");
            result.append("```\n");
            for (RevCommit commit : commits) {
                GitCommit gitCommit = new GitCommit(commit);
                result.append(gitCommit.getShortId());
                result.append(" ");
                result.append(gitCommit.getShortMessage());
                result.append("\n");
            }
            result.append("```\n\n");
        }

        if (refNames.size() > 0) {
            result.append("### " + Messages.get("notification.pushed.branches") + "\n");

            for (String refName : refNames) {
                try {
                    result.append("[" + refName + "](" + routes.CodeHistoryApp.history(project.owner, project.name,
                            URLEncoder.encode(refName, "UTF-8"), "") + ")");
                } catch (UnsupportedEncodingException e) {
                    result.append(refName);
                }
                result.append("\n");
            }
        }

        return result.toString();
    }

    private static NotificationEvent createFrom(User sender, ResourceConvertible rc) {
        NotificationEvent notiEvent = new NotificationEvent();
        notiEvent.senderId = sender.id;
        notiEvent.created = new Date();
        Resource resource = rc.asResource();
        notiEvent.resourceId = resource.getId();
        notiEvent.resourceType = resource.getType();
        return notiEvent;
    }

    /**
     * @see {@link #createFrom(models.User, models.resource.ResourceConvertible)}
     */
    private static NotificationEvent createFromCurrentUser(ResourceConvertible rc) {
        return createFrom(UserApp.currentUser(), rc);
    }

    private static Set<User> getReceivers(AbstractPosting abstractPosting) {
        Set<User> receivers = abstractPosting.getWatchers();
        receivers.addAll(getMentionedUsers(abstractPosting.body));
        receivers.remove(UserApp.currentUser());
        return receivers;
    }

    private static String getPrefixedNumber(AbstractPosting posting) {
        if (posting instanceof Issue) {
            return "#" + posting.getNumber();
        } else {
            return posting.getNumber().toString();
        }
    }

    private static String formatReplyTitle(AbstractPosting posting) {
        return String.format("Re: [%s] %s (%s)", posting.project.name, posting.title, getPrefixedNumber(posting));
    }

    private static String formatNewTitle(AbstractPosting posting) {
        return String.format("[%s] %s (%s)", posting.project.name, posting.title, getPrefixedNumber(posting));
    }

    private static String formatReplyTitle(Project project, Commit commit) {
        return String.format("Re: [%s] %s (%s)", project.name, commit.getShortMessage(), commit.getShortId());
    }

    private static Set<User> getReceivers(User sender, PullRequest pullRequest) {
        Set<User> watchers = getDefaultReceivers(pullRequest);
        watchers.remove(sender);
        return watchers;
    }

    private static Set<User> getDefaultReceivers(PullRequest pullRequest) {
        Set<User> watchers = pullRequest.getWatchers();
        watchers.addAll(getMentionedUsers(pullRequest.body));
        return watchers;
    }

    private static Set<User> getReceiversWithRelatedAuthors(User sender, PullRequest pullRequest) {
        Set<User> receivers = getDefaultReceivers(pullRequest);
        String failureMessage = "Failed to get authors related to the pullrequest " + pullRequest;
        try {
            if (pullRequest.mergedCommitIdFrom != null && pullRequest.mergedCommitIdTo != null) {
                receivers.addAll(
                        GitRepository.getRelatedAuthors(new GitRepository(pullRequest.toProject).getRepository(),
                                pullRequest.mergedCommitIdFrom, pullRequest.mergedCommitIdTo));
            }
        } catch (LimitExceededException e) {
            for (ProjectUser member : pullRequest.toProject.members()) {
                receivers.add(member.user);
            }
            play.Logger.info(failureMessage + ": Get all project members instead", e);
        } catch (GitAPIException e) {
            play.Logger.warn(failureMessage, e);
        } catch (IOException e) {
            play.Logger.warn(failureMessage, e);
        }
        receivers.remove(sender);
        return receivers;
    }

    private static String formatNewTitle(PullRequest pullRequest) {
        return String.format("[%s] %s (#%d)", pullRequest.toProject.name, pullRequest.title, pullRequest.number);
    }

    private static String formatReplyTitle(PullRequest pullRequest) {
        return String.format("Re: [%s] %s (#%s)", pullRequest.toProject.name, pullRequest.title,
                pullRequest.number);
    }

    private static Set<User> getReceivers(Project project) {
        Set<User> receivers = new HashSet<>();
        List<User> managers = User.findUsersByProject(project.id, RoleType.MANAGER);
        for (User manager : managers) {
            if (Watch.isWatching(manager, project.asResource())) {
                receivers.add(manager);
            }
        }
        return receivers;
    }

    private static Set<User> getReceivers(Organization organization) {
        Set<User> receivers = new HashSet<>();
        List<User> managers = User.findUsersByOrganization(organization.id, RoleType.ORG_ADMIN);
        receivers.addAll(managers);

        return receivers;
    }

    private static String formatMemberRequestTitle(Project project, User user) {
        return Messages.get("notification.member.request.title", project.name, user.loginId);
    }

    private static String formatMemberRequestCancelTitle(Project project, User user) {
        return Messages.get("notification.member.request.cancel.title", project.name, user.loginId);
    }

    private static String formatMemberRequestCancelTitle(Organization organization, User user) {
        return Messages.get("notification.member.request.cancel.title", organization.name, user.loginId);
    }

    private static String formatMemberRequestTitle(Organization organization, User user) {
        return Messages.get("notification.organization.member.request.title", organization.name, user.loginId);
    }

    private static String formatMemberAcceptTitle(Project project, User user) {
        return Messages.get("notification.member.request.accept.title", project.name, user.loginId);
    }

    private static String formatMemberAcceptTitle(Organization organization, User user) {
        return Messages.get("notification.member.request.accept.title", organization.name, user.loginId);
    }

    public static Set<User> getMentionedUsers(String body) {
        Matcher matcher = Pattern.compile("@" + User.LOGIN_ID_PATTERN_ALLOW_FORWARD_SLASH).matcher(body);
        Set<User> users = new HashSet<>();
        while (matcher.find()) {
            String mentionWord = matcher.group().substring(1);
            users.addAll(findOrganizationMembers(mentionWord));
            users.addAll(findProjectMembers(mentionWord));
            users.add(User.findByLoginId(mentionWord));
        }
        users.remove(User.anonymous);
        return users;
    }

    private static Set<User> findOrganizationMembers(String mentionWord) {
        Set<User> users = new HashSet<>();
        Organization org = Organization.findByName(mentionWord);
        if (org != null) {
            for (OrganizationUser orgUser : org.users) {
                users.add(orgUser.user);
            }
        }
        return users;
    }

    private static Set<User> findProjectMembers(String mentionWord) {
        Set<User> users = new HashSet<>();
        if (mentionWord.contains("/")) {
            String projectName = mentionWord.substring(mentionWord.lastIndexOf("/") + 1);
            String loginId = mentionWord.substring(0, mentionWord.lastIndexOf("/"));
            Project mentionedProject = Project.findByOwnerAndProjectName(loginId, projectName);
            if (mentionedProject == null) {
                return users;
            }
            for (ProjectUser projectUser : mentionedProject.members()) {
                users.add(projectUser.user);
            }
        }
        return users;
    }

    public static void scheduleDeleteOldNotifications() {
        if (EventConstants.KEEP_TIME_IN_DAYS > 0) {
            Akka.system().scheduler().schedule(Duration.create(1, TimeUnit.MINUTES),
                    Duration.create(1, TimeUnit.DAYS), new Runnable() {
                        @Override
                        public void run() {
                            Date threshold = DateTime.now().minusDays(EventConstants.KEEP_TIME_IN_DAYS).toDate();
                            List<NotificationEvent> olds = find.where().lt("created", threshold).findList();

                            for (NotificationEvent old : olds) {
                                old.delete();
                            }
                        }
                    }, Akka.system().dispatcher());
        }
    }

    public static void onStart() {
        scheduleDeleteOldNotifications();
    }

    public static List<NotificationEvent> findByReceiver(User user, int from, int size) {
        return find.where().eq("receivers.id", user.id).order().desc("created").setFirstRow(from).setMaxRows(size)
                .findList();
    }
}