NotificationServiceImpl.java

/*
 * This software was designed and created by Jason Carroll.
 * Copyright (c) 2002, 2003, 2004 Jason Carroll.
 * The author can be reached at jcarroll@cowsultants.com
 * ITracker website: http://www.cowsultants.com
 * ITracker forums: http://www.cowsultants.com/phpBB/index.php
 *
 * This program is free software; you can redistribute it and/or modify
 * it only under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

package org.itracker.services.implementations;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.itracker.core.resources.ITrackerResources;
import org.itracker.model.*;
import org.itracker.model.Notification.Role;
import org.itracker.model.Notification.Type;
import org.itracker.model.util.IssueUtilities;
import org.itracker.model.util.ProjectUtilities;
import org.itracker.model.util.UserUtilities;
import org.itracker.persistence.dao.IssueActivityDAO;
import org.itracker.persistence.dao.IssueDAO;
import org.itracker.persistence.dao.NotificationDAO;
import org.itracker.services.EmailService;
import org.itracker.services.IssueService;
import org.itracker.services.NotificationService;
import org.itracker.services.ProjectService;
import org.itracker.util.HTMLUtilities;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.mail.internet.InternetAddress;
import java.net.MalformedURLException;
import java.util.*;

public class NotificationServiceImpl implements NotificationService, ApplicationContextAware {
    public static final Integer DEFAULT_ISSUE_AGE = 30;


    private EmailService emailService;
    private NotificationDAO notificationDao;
    private ProjectService projectService;
    private IssueActivityDAO issueActivityDao;
    private IssueDAO issueDao;


    private String issueServiceName;

    private static final Logger logger = Logger
            .getLogger(NotificationServiceImpl.class);
    private IssueService issueService;
    private ApplicationContext applicationContext;

    public NotificationServiceImpl() {

        this.emailService = null;
        this.projectService = null;
        this.notificationDao = null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public NotificationServiceImpl(EmailService emailService,
                                   ProjectService projectService, NotificationDAO notificationDao, IssueActivityDAO issueActivityDao, IssueDAO issueDao, IssueService issueService) {
        this();
        this.setEmailService(emailService);
        this.setProjectService(projectService);
        this.setNotificationDao(notificationDao);
        this.setIssueActivityDao(issueActivityDao);
        this.setIssueDao(issueDao);
        this.setIssueService(issueService);
    }

    public void sendNotification(Notification notification, Type type,
                                 String url) {

        if (logger.isDebugEnabled()) {
            logger.debug("sendNotification: called with notification: "
                    + notification + ", type: " + url + ", url: " + url);
        }
        if (null == notification) {
            throw new IllegalArgumentException("notification must not be null");
        }
        if (null == this.emailService || null == this.notificationDao) {
            throw new IllegalStateException("service not initialized yet");
        }
        if (type == Type.SELF_REGISTER) {
            this.handleSelfRegistrationNotification(notification.getUser()
                    .getLogin(), notification.getUser().getEmailAddress(), notification.getUser().getPreferences().getUserLocale(), url);
        } else {
            handleIssueNotification(notification.getIssue(), type, url);

        }

    }

    public void sendNotification(Issue issue, Type type, String baseURL) {
        if (logger.isDebugEnabled()) {
            logger.debug("sendNotification: called with issue: " + issue
                    + ", type: " + type + ", baseURL: " + baseURL);
        }
        handleIssueNotification(issue, type, baseURL);

    }

    public void setEmailService(EmailService emailService) {

        if (null == emailService)
            throw new IllegalArgumentException("email service must not be null");

        if (null != this.emailService) {
            throw new IllegalStateException("email service allready set");
        }
        this.emailService = emailService;

    }


    private void handleSelfRegistrationNotification(String login,
                                                    InternetAddress toAddress, String locale, String url) {
        if (logger.isDebugEnabled()) {
            logger
                    .debug("handleSelfRegistrationNotification: called with login: "
                            + login
                            + ", toAddress"
                            + toAddress
                            + ", url: "
                            + url);
        }
        try {

            if (toAddress != null && !"".equals(toAddress.getAddress())) {
                String subject = ITrackerResources
                        .getString("itracker.email.selfreg.subject", locale);
                String msgText = ITrackerResources.getString(
                        "itracker.email.selfreg.body", locale, new Object[]{login,
                                url + "/login.do"});
                emailService.sendEmail(toAddress, subject, msgText);
            } else {
                throw new IllegalArgumentException(
                        "To-address must be set for self registration notification.");
            }
        } catch (RuntimeException e) {
            logger.error("failed to handle self registration notification for "
                    + toAddress, e);
            throw e;
        }
    }

    /**
     * Method for internal sending of a notification of specific type.
     */
    private void handleIssueNotification(Issue issue, Type type, String url) {

        if (logger.isDebugEnabled()) {
            logger.debug("handleIssueNotification: called with issue: " + issue
                    + ", type: " + type + "url: " + url);
        }
        this.handleLocalizedIssueNotification(issue, type, url, null, null);
    }


    /**
     * Method for internal sending of a notification of specific type.
     */
    private void handleLocalizedIssueNotification(final Issue issue, final Type type, final String url,
                                                  final InternetAddress[] recipients, Integer lastModifiedDays) {
        try {

            if (logger.isDebugEnabled()) {
                logger
                        .debug("handleLocalizedIssueNotification: running as thread, called with issue: "
                                + issue
                                + ", type: "
                                + type
                                + "url: "
                                + url
                                + ", recipients: "
                                + (null == recipients ? "<null>" : String
                                .valueOf(Arrays.asList(recipients)))
                                + ", lastModifiedDays: " + lastModifiedDays);
            }

            final Integer notModifiedSince;

            if (lastModifiedDays == null || lastModifiedDays < 0) {
                notModifiedSince = DEFAULT_ISSUE_AGE;
            } else {
                notModifiedSince = lastModifiedDays;
            }

            try {
                if (logger.isDebugEnabled()) {
                    logger
                            .debug("handleLocalizedIssueNotification.run: running as thread, called with issue: "
                                    + issue
                                    + ", type: "
                                    + type
                                    + "url: "
                                    + url
                                    + ", recipients: "
                                    + (null == recipients ? "<null>" : String
                                    .valueOf(Arrays.asList(recipients)))
                                    + ", notModifiedSince: " + notModifiedSince);
                }
                final List<Notification> notifications;
                if (issue == null) {
                    logger
                            .warn("handleLocalizedIssueNotification: issue was null. Notification will not be handled");
                    return;
                }
                Map<InternetAddress, Locale> localeMapping;

                if (recipients == null) {
                    notifications = this.getIssueNotifications(issue);

                    localeMapping = new Hashtable<>(notifications.size());
                    Iterator<Notification> it = notifications.iterator();
                    User currentUser;
                    while (it.hasNext()) {
                        currentUser = it.next().getUser();
                        if (null != currentUser
                                && null != currentUser.getEmailAddress()
                                && null != currentUser.getEmail()
                                && (!localeMapping.keySet()
                                .contains(currentUser.getEmailAddress()))) {

                            try {
                                localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale(currentUser.getPreferences().getUserLocale()));
                            } catch (RuntimeException re) {
                                localeMapping.put(currentUser.getEmailAddress(), ITrackerResources.getLocale());
                            }
                        }
                    }
                } else {
                    localeMapping = new Hashtable<>(1);
                    Locale locale = ITrackerResources.getLocale();
                    for (InternetAddress internetAddress : Arrays.asList(recipients)) {
                        localeMapping.put(internetAddress, locale);
                    }
                }

                this.handleNotification(issue, type, localeMapping, url, notModifiedSince);
            } catch (Exception e) {
                logger.error("run: failed to process notification", e);
            }

        } catch (Exception e) {
            logger
                    .error(
                            "handleLocalizedIssueNotification: unexpected exception caught, throwing runtime exception",
                            e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public void sendReminder(Issue issue, User user, String baseURL, int issueAge) {
        Map<InternetAddress, Locale> recipient = new HashMap<>(1);
        recipient.put(user.getEmailAddress(), ITrackerResources.getLocale(user.getPreferences().getUserLocale()));
        handleNotification(issue, Type.ISSUE_REMINDER, recipient, baseURL, issueAge);
    }

    /**
     * Send notifications to mapped addresses by locale.
     */
    private void handleNotification(Issue issue, Type type, Map<InternetAddress, Locale> recipientsLocales, final String url, Integer notModifiedSince) {
        Set<InternetAddress> recipients;
        Map<Locale, Set<InternetAddress>> localeRecipients = new Hashtable<>();

        List<Component> components = issue.getComponents();

        List<IssueActivity> activity = getIssueService().getIssueActivity(
                issue.getId(), false);

        IssueHistory history;
        history = getIssueService().getLastIssueHistory(issue.getId());
        StringBuilder recipientsString = new StringBuilder();

        if (logger.isDebugEnabled() && null != history) {
            logger.debug("handleIssueNotification: got most recent history: "
                    + history
                    + " ("
                    + history.getDescription()
                    + ")");
        }

        for (InternetAddress internetAddress : recipientsLocales.keySet()) {
            recipientsString.append("\n  ");
            recipientsString.append(internetAddress.getPersonal());

            if (localeRecipients.keySet().contains(recipientsLocales.get(internetAddress))) {
                localeRecipients.get(recipientsLocales.get(internetAddress)).add(internetAddress);
            } else {
                Set<InternetAddress> addresses = new HashSet<>();
                addresses.add(internetAddress);
                localeRecipients.put(recipientsLocales.get(internetAddress), addresses);
            }
        }

        Iterator<Locale> localesIt = localeRecipients.keySet().iterator();
        try {
            while (localesIt.hasNext()) {
                Locale currentLocale = localesIt.next();
                recipients = localeRecipients.get(currentLocale);


                if (recipients.size() > 0) {
                    String subject;
                    if (type == Type.CREATED) {
                        subject = ITrackerResources.getString(
                                "itracker.email.issue.subject.created",
                                currentLocale,
                                new Object[]{issue.getId(),
                                        issue.getProject().getName()});
                    } else if (type == Type.ASSIGNED) {
                        subject = ITrackerResources.getString(
                                "itracker.email.issue.subject.assigned",
                                currentLocale,
                                new Object[]{issue.getId(),
                                        issue.getProject().getName()});
                    } else if (type == Type.CLOSED) {
                        subject = ITrackerResources.getString(
                                "itracker.email.issue.subject.closed",
                                currentLocale,
                                new Object[]{issue.getId(),
                                        issue.getProject().getName()});
                    } else if (type == Type.ISSUE_REMINDER) {
                        subject = ITrackerResources.getString(
                                "itracker.email.issue.subject.reminder",
                                currentLocale,
                                new Object[]{issue.getId(),
                                        issue.getProject().getName(),
                                        notModifiedSince});
                    } else {
                        subject = ITrackerResources.getString(
                                "itracker.email.issue.subject.updated",
                                currentLocale,
                                new Object[]{issue.getId(),
                                        issue.getProject().getName()});
                    }

                    String activityString;
                    String componentString = "";
                    StringBuilder sb = new StringBuilder();
                    if (activity.size() == 0) {
                        sb.append("-");
                    } else {
                        for (IssueActivity anActivity : activity) {
                            sb.append("\n ").append(
                                    IssueUtilities.getActivityName(anActivity
                                            .getActivityType(), currentLocale)).append(": ").append(
                                    anActivity.getDescription());

                        }
                    }
                    sb.append("\n");
                    activityString = sb.toString();
                    for (int i = 0; i < components.size(); i++) {
                        componentString += (i != 0 ? ", " : "")
                                + components.get(i).getName();
                    }

                    final String owner = IssueUtilities.getOwnerName(issue.getOwner(), currentLocale);
                    final User hUser = null == history ? null : history.getUser();
                    final String historyUser = (null != hUser) ? hUser.getFullName()
                            : ITrackerResources.getString("itracker.web.generic.notapplicable", currentLocale);

                    final String historyText = (history == null ? "-"
                            : HTMLUtilities
                            .removeMarkup(history
                                    .getDescription()));
                    final String status =
                            IssueUtilities.getStatusName(issue
                                    .getStatus(), currentLocale);
                    final String msgText;
                    if (type == Type.ISSUE_REMINDER) {
                        msgText = ITrackerResources
                                .getString(
                                        "itracker.email.issue.body.reminder",
                                        currentLocale,
                                        new Object[]{
                                                IssueUtilities.getIssueURL(issue, url).toExternalForm(),
                                                issue.getProject().getName(),
                                                issue.getDescription(),
                                                IssueUtilities.getStatusName(issue
                                                        .getStatus(), currentLocale),
                                                IssueUtilities
                                                        .getSeverityName(issue
                                                        .getSeverity(), currentLocale),
                                                owner,
                                                componentString,
                                                historyUser,
                                                historyText,
                                                notModifiedSince,
                                                activityString});
                    } else {
                        String resolution = (issue.getResolution() == null ? ""
                                : issue.getResolution());
                        if (!resolution.equals("")
                                && ProjectUtilities
                                .hasOption(
                                        ProjectUtilities.OPTION_PREDEFINED_RESOLUTIONS,
                                        issue.getProject().getOptions())) {
                            resolution = IssueUtilities.getResolutionName(
                                    resolution, currentLocale);
                        }
                        msgText = ITrackerResources
                                .getString(
                                        "itracker.email.issue.body."
                                                + (type == Type.CREATED ? "created" : "standard"),
                                        currentLocale,
                                        new Object[]{
                                                url + "/module-projects/view_issue.do?id=" + issue.getId(),
                                                issue.getProject().getName(),
                                                issue.getDescription(),
                                                status,
                                                resolution,
                                                IssueUtilities
                                                        .getSeverityName(issue
                                                        .getSeverity(), currentLocale),
                                                owner,
                                                componentString,
                                                historyUser,
                                                historyText,
                                                activityString,
                                                recipientsString});
                    }

                    if (logger.isInfoEnabled()) {
                        logger.info("handleNotification: sending notification for " + issue + " (" + type + ") to " + currentLocale + "-users (" + recipients + ")");

                    }
                    for (InternetAddress iadr : recipients) {
                        emailService.sendEmail(iadr, subject, msgText);
                    }

                    if (logger.isDebugEnabled()) {
                        logger.debug("handleNotification: sent notification for " + issue
                                + ": " + subject + "\n  " + msgText);
                    }
                }

                updateIssueActivityNotification(issue, true);
                if (logger.isDebugEnabled()) {
                    logger.debug("handleNotification: sent notification for locales " + localeRecipients.keySet() + " recipients: " + localeRecipients.values());
                }
            }
        } catch (RuntimeException e) {
            logger.error("handleNotification: failed to notify: " + issue + " (locales: " + localeRecipients.keySet() + ")", e);

        } catch (MalformedURLException e) {
            logger.error("handleNotification: URL was not well-formed", e);
        }


    }

    private IssueService getIssueService() {
        if (null == issueService) {
            setIssueService((IssueService) applicationContext.getBean("issueService"));
        }

        return issueService;
    }

    public void setIssueService(IssueService issueService) {
        this.issueService = issueService;
    }

    public void updateIssueActivityNotification(Issue issue,
                                                Boolean notificationSent) {
        if (logger.isDebugEnabled()) {
            logger.debug("updateIssueActivityNotification: called with "
                    + issue + ", notificationSent: " + notificationSent);
        }

        Collection<IssueActivity> activities = getIssueActivityDao()
                .findByIssueId(issue.getId());
        for (IssueActivity activity : activities) {
            activity.setNotificationSent(notificationSent);
        }
    }

    /**
     */
    public boolean addIssueNotification(Notification notification) {
        if (logger.isDebugEnabled()) {
            logger.debug("addIssueNotification: called with notification: "
                    + notification);
        }
        Issue issue = notification.getIssue();
        if (!issue.getNotifications().contains(notification)) {
            if (notification.getCreateDate() == null) {
                notification.setCreateDate(new Date());
            }
            if (notification.getLastModifiedDate() == null) {
                notification.setLastModifiedDate(new Date());
            }

            getNotificationDao().save(notification);

            issue.getNotifications().add(notification);
            getIssueDao().merge(issue);

            return true;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("addIssueNotification: attempted to add duplicate notification " + notification + " for issue: " + issue);
        }
        return false;
    }

    /**
     *
     */
    public List<Notification> getIssueNotifications(Issue issue,
                                                    boolean primaryOnly, boolean activeOnly) {
        if (logger.isDebugEnabled()) {
            logger.debug("getIssueNotifications: called with issue: " + issue
                    + ", primaryOnly: " + primaryOnly + ", activeOnly: "
                    + activeOnly);
        }
        List<Notification> issueNotifications = new ArrayList<>();
        if (issue == null) {
            logger.warn("getIssueNotifications: no issue, throwing exception");
            throw new IllegalArgumentException("issue must not be null");
        }
        if (!primaryOnly) {
            List<Notification> notifications = getNotificationDao()
                    .findByIssueId(issue.getId());

            for (Notification notification : notifications) {
                User notificationUser = notification.getUser();
                if (!activeOnly
                        || notificationUser.getStatus() == UserUtilities.STATUS_ACTIVE) {
                    issueNotifications.add(notification);
                }
            }
        }

        // Now add in other notifications like owner, creator, project owners,
        // etc...

        boolean hasOwner = false;
        if (issue.getOwner() != null) {
            User ownerModel = issue.getOwner();

            if (ownerModel != null
                    && (!activeOnly || ownerModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
                issueNotifications.add(new Notification(ownerModel, issue,
                        Role.OWNER));
                hasOwner = true;
            }
        }

        if (!primaryOnly || !hasOwner) {
            User creatorModel = issue.getCreator();

            if (creatorModel != null
                    && (!activeOnly || creatorModel.getStatus() == UserUtilities.STATUS_ACTIVE)) {
                issueNotifications.add(new Notification(creatorModel,
                        issue, Role.CREATOR));
            }
        }

        Project project = getProjectService().getProject(
                issue.getProject().getId());

        for (User projectOwner : project.getOwners()) {
            if (projectOwner != null
                    && (!activeOnly || projectOwner.getStatus() == UserUtilities.STATUS_ACTIVE)) {
                issueNotifications.add(new Notification(projectOwner,
                        issue, Role.PO));
            }
        }

        if (logger.isDebugEnabled()) {
            logger.debug("getIssueNotifications: returning "
                    + issueNotifications);
        }
        return issueNotifications;
    }

    public List<Notification> getIssueNotifications(Issue issue) {
        if (logger.isDebugEnabled()) {
            logger.debug("getIssueNotifications: called with: " + issue);
        }
        return this.getIssueNotifications(issue, false, true);
    }

    public List<Notification> getPrimaryIssueNotifications(Issue issue) {
        if (logger.isDebugEnabled()) {
            logger.debug("getPrimaryIssueNotifications: called with: " + issue);
        }
        return this.getIssueNotifications(issue, true, false);
    }

    public boolean hasIssueNotification(Issue issue, Integer userId) {
        if (logger.isDebugEnabled()) {
            logger.debug("hasIssueNotification: called with: " + issue
                    + ", userId: " + userId);
        }
        return hasIssueNotification(issue, userId, Role.ANY);
    }

    @Override
    public boolean hasIssueNotification(Issue issue, String login) {

        return hasIssueNotification(issue, login, Role.ANY);
    }

    @Override
    public boolean hasIssueNotification(Issue issue, String login, Role role) {

        if (issue != null && StringUtils.isNotBlank(login)) {

            List<Notification> notifications = getIssueNotifications(issue,
                    false, false);

            for (Notification notification : notifications) {

                if (role == Role.ANY || notification.getRole() == role) {

                    if (StringUtils.equals(login, notification.getUser().getLogin())) {

                        return true;

                    }

                }

            }

        }

        return false;
    }

    public boolean hasIssueNotification(Issue issue, Integer userId, Role role) {

        if (issue != null && userId != null) {

            List<Notification> notifications = getIssueNotifications(issue,
                    false, false);

            for (Notification notification : notifications) {

                if (role == Role.ANY || notification.getRole() == role) {

                    if (notification.getUser().getId().equals(userId)) {

                        return true;

                    }

                }

            }

        }

        return false;

    }

    public boolean removeIssueNotification(Integer notificationId) {
        Notification notification = this.getNotificationDao().findById(
                notificationId);
        getNotificationDao().delete(notification);
        return true;
    }

    public void sendNotification(Issue issue, Type type, String baseURL,
                                 InternetAddress[] receipients, Integer lastModifiedDays) {
        this.handleLocalizedIssueNotification(issue, type, baseURL, receipients,
                lastModifiedDays);

    }


    /**
     * @return the emailService
     */
    public EmailService getEmailService() {
        return emailService;
    }

    /**
     * @return the notificationDao
     */
    private NotificationDAO getNotificationDao() {
        return notificationDao;
    }

    /**
     * @return the projectService
     */
    public ProjectService getProjectService() {
        return projectService;
    }

    /**
     * @param projectService the projectService to set
     */
    public void setProjectService(ProjectService projectService) {
        this.projectService = projectService;
    }

    /**
     * @param notificationDao the notificationDao to set
     */
    public void setNotificationDao(NotificationDAO notificationDao) {
        if (null == notificationDao) {
            throw new IllegalArgumentException(
                    "notification dao must not be null");
        }
        if (null != this.notificationDao) {
            throw new IllegalStateException("notification dao allready set");
        }
        this.notificationDao = notificationDao;
    }


    /**
     * @return the issueActivityDao
     */
    public IssueActivityDAO getIssueActivityDao() {
        return issueActivityDao;
    }

    /**
     * @param issueActivityDao the issueActivityDao to set
     */
    public void setIssueActivityDao(IssueActivityDAO issueActivityDao) {
        this.issueActivityDao = issueActivityDao;
    }

    /**
     * @return the issueDao
     */
    public IssueDAO getIssueDao() {
        return issueDao;
    }

    /**
     * @param issueDao the issueDao to set
     */
    public void setIssueDao(IssueDAO issueDao) {
        this.issueDao = issueDao;
    }

    public String getIssueServiceName() {
        return issueServiceName;
    }

    public void setIssueServiceName(String issueServiceName) {
        this.issueServiceName = issueServiceName;
    }

}