org.eurekastreams.server.action.execution.notification.notifier.EmailNotifier.java Source code

Java tutorial

Introduction

Here is the source code for org.eurekastreams.server.action.execution.notification.notifier.EmailNotifier.java

Source

/*
 * Copyright (c) 2011-2012 Lockheed Martin Corporation
 *
 * 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.eurekastreams.server.action.execution.notification.notifier;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.implement.EscapeHtmlReference;
import org.apache.velocity.context.Context;
import org.eurekastreams.commons.exceptions.ExecutionException;
import org.eurekastreams.commons.server.UserActionRequest;
import org.eurekastreams.server.action.execution.email.NotificationEmailDTO;
import org.eurekastreams.server.action.execution.notification.NotificationPropertyKeys;
import org.eurekastreams.server.action.execution.notification.notifier.EmailNotificationTemplate.ReplyAction;
import org.eurekastreams.server.domain.HasEmail;
import org.eurekastreams.server.domain.NotificationType;
import org.eurekastreams.server.domain.stream.ActivityDTO;
import org.eurekastreams.server.search.modelview.PersonModelView;
import org.eurekastreams.server.service.actions.strategies.ActivityInteractionType;
import org.eurekastreams.server.service.email.TokenContentEmailAddressBuilder;
import org.eurekastreams.server.service.email.TokenContentFormatter;
import org.eurekastreams.server.service.utility.authorization.ActivityInteractionAuthorizationStrategy;

/**
 * Notifier for in-app notifications. Builds the messages and stores them in the database.
 */
public class EmailNotifier implements Notifier {
    /** Apache Velocity templating engine. */
    private final VelocityEngine velocityEngine;

    /** Global context for Apache Velocity templating engine. (Holds system-wide properties.) */
    private final Context velocityGlobalContext;

    /** Message templates by notification type. */
    private final Map<NotificationType, EmailNotificationTemplate> templates;

    /** Prefix to use on email subjects. */
    private final String subjectPrefix;

    /** Builds the token content. */
    private final TokenContentFormatter tokenContentFormatter;

    /** Builds the recipient email address with a token. */
    private final TokenContentEmailAddressBuilder tokenAddressBuilder;

    /** For determining if users can comment on an activity. */
    private final ActivityInteractionAuthorizationStrategy activityAuthorizer;

    /** If HTML emails will be sent. (These will be multipart with a plain text component.) */
    private final boolean sendHtml;

    /**
     * Constructor.
     *
     * @param inVelocityEngine
     *            Apache Velocity templating engine.
     * @param inVelocityGlobalContext
     *            Global context for Apache Velocity templating engine.
     * @param inTemplates
     *            Message templates by notification type.
     * @param inSubjectPrefix
     *            Prefix to use on email subjects.
     * @param inTokenContentFormatter
     *            Builds the token content.
     * @param inTokenAddressBuilder
     *            Builds the recipient email address with a token.
     * @param inActivityAuthorizer
     *            For determining if users can comment on an activity.
     * @param inSendHtml
     *            If HTML emails will be sent. (These will be multipart with a plain text component.)
     */
    public EmailNotifier(final VelocityEngine inVelocityEngine, final Context inVelocityGlobalContext,
            final Map<NotificationType, EmailNotificationTemplate> inTemplates, final String inSubjectPrefix,
            final TokenContentFormatter inTokenContentFormatter,
            final TokenContentEmailAddressBuilder inTokenAddressBuilder,
            final ActivityInteractionAuthorizationStrategy inActivityAuthorizer, final boolean inSendHtml) {
        velocityEngine = inVelocityEngine;
        velocityGlobalContext = inVelocityGlobalContext;
        templates = inTemplates;
        subjectPrefix = inSubjectPrefix;
        tokenContentFormatter = inTokenContentFormatter;
        tokenAddressBuilder = inTokenAddressBuilder;
        activityAuthorizer = inActivityAuthorizer;
        sendHtml = inSendHtml;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<UserActionRequest> notify(final NotificationType inType, final Collection<Long> inRecipients,
            final Map<String, Object> inProperties, final Map<Long, PersonModelView> inRecipientIndex)
            throws Exception {
        EmailNotificationTemplate template = templates.get(inType);
        if (template == null) {
            // Not an error - this is an easy way to disable a given notification.
            return null;
        }

        // prepare recipient lists
        // determine which recipients get individual emails with reply tokens and which get a mass general email
        List<String> addresses = new ArrayList<String>(inRecipients.size());
        Map<String, String> addressesWithTokens = Collections.EMPTY_MAP;
        if (template.getReplyAddressType() == ReplyAction.COMMENT) {
            Object obj = inProperties.get("activity");
            if (obj == null || !(obj instanceof ActivityDTO)) {
                throw new ExecutionException("Notification requires activity property for building token.");
            }
            ActivityDTO activity = (ActivityDTO) obj;

            addressesWithTokens = new HashMap<String, String>(inRecipients.size());
            String tokenData = tokenContentFormatter.buildForActivity(activity.getId());
            // ok to use relaxed mode here: the translators wouldn't include recipients who do not have access to the
            // activity
            boolean generallyAllowed = activityAuthorizer.authorize(activity, ActivityInteractionType.COMMENT,
                    false);

            for (long recipientId : inRecipients) {
                String address = inRecipientIndex.get(recipientId).getEmail();
                if (StringUtils.isNotBlank(address)) {
                    // Note: checking on a per-user basis is very inefficient. That's why the generallyAllowed
                    // optimization was put in to omit the per-user check for streams that allow commenting. Both the
                    // generallyAllowed and the per-user call could be replaced with an authorizer that takes a list of
                    // users to check and returns a list of only those which are allowed (i.e. an authorization filter).
                    // If the scenario arises where there are streams which do not allow commenting with many email
                    // subscribers, then a bulk authorizer could be used to significantly improve performance.
                    if (generallyAllowed || activityAuthorizer.authorize(recipientId, activity,
                            ActivityInteractionType.COMMENT)) {
                        String replyAddress = tokenAddressBuilder.build(tokenData, recipientId);
                        addressesWithTokens.put(address, replyAddress);
                    } else {
                        addresses.add(address);
                    }
                }
            }
        } else {
            for (long recipientId : inRecipients) {
                String address = inRecipientIndex.get(recipientId).getEmail();
                if (StringUtils.isNotBlank(address)) {
                    addresses.add(address);
                }
            }
        }

        int emailCount = addressesWithTokens.size() + (addresses.isEmpty() ? 0 : 1);
        if (emailCount == 0) {
            return null;
        }
        List<UserActionRequest> requests = new ArrayList<UserActionRequest>(emailCount);

        // -- prepare the email --

        NotificationEmailDTO email = new NotificationEmailDTO();

        // Note: The doubly-nested context is to prevent the code here and Velocity templates from updating
        // inProperties. The inner context uses inProperties as its backing store, so anything added to the context --
        // such as the values added here and any SET calls in templates -- would be added to inProperties, which we
        // don't want.
        Context velocityContext = new VelocityContext(new VelocityContext(inProperties, velocityGlobalContext));
        velocityContext.put("context", velocityContext);
        velocityContext.put("type", inType);
        if (addressesWithTokens.size() + addresses.size() == 1) {
            velocityContext.put("recipient", inRecipientIndex.get(inRecipients.iterator().next()));
        }

        // build the subject
        StringWriter writer = new StringWriter();
        velocityEngine.evaluate(velocityContext, writer, "EmailSubject-" + inType, template.getSubjectTemplate());
        email.setSubject(subjectPrefix + writer.toString());

        // set the priority
        email.setHighPriority(Boolean.TRUE.equals(inProperties.get(NotificationPropertyKeys.HIGH_PRIORITY)));

        // render the body

        String noReplyTextBody = null;
        String replyTextBody = null;
        String noReplyHtmlBody = null;
        String replyHtmlBody = null;

        // build the text body
        Template vt = velocityEngine.getTemplate(template.getTextBodyTemplateResourcePath());
        if (!addresses.isEmpty()) {
            velocityContext.put("hasReplyAddress", false);
            writer.getBuffer().setLength(0);
            vt.merge(velocityContext, writer);
            noReplyTextBody = writer.toString();
        }
        if (!addressesWithTokens.isEmpty()) {
            velocityContext.put("hasReplyAddress", true);
            writer.getBuffer().setLength(0);
            vt.merge(velocityContext, writer);
            replyTextBody = writer.toString();
        }

        // build the HTML body
        if (sendHtml) {
            final String htmlBodyTemplateResourcePath = template.getHtmlBodyTemplateResourcePath();
            if (htmlBodyTemplateResourcePath != null) {
                vt = velocityEngine.getTemplate(htmlBodyTemplateResourcePath);
                // HTML-escape all content inserted
                EventCartridge ec = new EventCartridge();
                ec.addEventHandler(new EscapeHtmlReference());
                ec.attachToContext(velocityContext);
                if (!addresses.isEmpty()) {
                    velocityContext.put("hasReplyAddress", false);
                    writer.getBuffer().setLength(0);
                    vt.merge(velocityContext, writer);
                    noReplyHtmlBody = writer.toString();
                }
                if (!addressesWithTokens.isEmpty()) {
                    velocityContext.put("hasReplyAddress", true);
                    writer.getBuffer().setLength(0);
                    vt.merge(velocityContext, writer);
                    replyHtmlBody = writer.toString();
                }
            }
        }

        // -- create requests to send emails --
        if (!addressesWithTokens.isEmpty()) {
            email.setTextBody(replyTextBody);
            if (replyHtmlBody != null) {
                email.setHtmlBody(replyHtmlBody);
            }
            for (Entry<String, String> entry : addressesWithTokens.entrySet()) {
                NotificationEmailDTO userEmail = email.clone();
                userEmail.setReplyTo(entry.getValue());
                String address = entry.getKey();
                userEmail.setToRecipient(address);
                // set the description (for logging / debugging)
                userEmail.setDescription(inType + " with token to " + address);

                requests.add(new UserActionRequest("sendEmailNotificationAction", null, userEmail));
            }
        }
        if (!addresses.isEmpty()) {
            email.setTextBody(noReplyTextBody);
            if (noReplyHtmlBody != null) {
                email.setHtmlBody(noReplyHtmlBody);
            }

            if (addresses.size() == 1) {
                final String address = addresses.get(0);
                email.setToRecipient(address);
                // set the description (for logging / debugging)
                email.setDescription(inType + " to " + address);
            } else {
                email.setBccRecipients(StringUtils.join(addresses, ','));
                // set the description (for logging / debugging)
                email.setDescription(inType + " to " + inRecipients.size() + " recipients");
            }

            // set the reply-to to the actor (so replies to emails go to the actor, not the system)

            if (template.getReplyAddressType() == ReplyAction.ACTOR) {
                Object obj = inProperties.get(NotificationPropertyKeys.ACTOR);
                if (obj instanceof HasEmail) {
                    HasEmail actor = (HasEmail) obj;
                    String actorEmail = actor.getEmail();
                    if (StringUtils.isNotBlank(actorEmail)) {
                        email.setReplyTo(actorEmail);
                    }
                }
            }

            requests.add(new UserActionRequest("sendEmailNotificationAction", null, email));
        }
        return requests;
    }
}