org.jasig.ssp.service.impl.PersonEmailServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.PersonEmailServiceImpl.java

Source

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you 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 the following location:
 *
 *   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.jasig.ssp.service.impl;

import com.google.common.collect.Maps;
import org.jasig.ssp.factory.PersonSearchRequestTOFactory;
import org.jasig.ssp.model.JournalEntry;
import org.jasig.ssp.model.Message;
import org.jasig.ssp.model.Person;
import org.jasig.ssp.model.PersonSearchRequest;
import org.jasig.ssp.model.SubjectAndBody;
import org.jasig.ssp.model.reference.ConfidentialityLevel;
import org.jasig.ssp.model.reference.JournalSource;
import org.jasig.ssp.model.reference.MessageTemplate;
import org.jasig.ssp.service.JournalEntryService;
import org.jasig.ssp.service.MessageService;
import org.jasig.ssp.service.ObjectNotFoundException;
import org.jasig.ssp.service.PersonEmailService;
import org.jasig.ssp.service.PersonSearchService;
import org.jasig.ssp.service.PersonService;
import org.jasig.ssp.service.SecurityService;
import org.jasig.ssp.service.VelocityTemplateService;
import org.jasig.ssp.service.jobqueue.AbstractPersonSearchBasedJobExecutor;
import org.jasig.ssp.service.jobqueue.AbstractPersonSearchBasedJobQueuer;
import org.jasig.ssp.service.jobqueue.BasePersonSearchBasedJobExecutionState;
import org.jasig.ssp.service.jobqueue.JobExecutionResult;
import org.jasig.ssp.service.jobqueue.JobService;
import org.jasig.ssp.service.reference.ConfidentialityLevelService;
import org.jasig.ssp.service.reference.ConfigService;
import org.jasig.ssp.service.reference.JournalSourceService;
import org.jasig.ssp.service.reference.MessageTemplateService;
import org.jasig.ssp.transferobject.ImmutablePersonIdentifiersTO;
import org.jasig.ssp.transferobject.MessageTO;
import org.jasig.ssp.transferobject.form.BulkEmailJobSpec;
import org.jasig.ssp.transferobject.form.BulkEmailStudentRequestForm;
import org.jasig.ssp.transferobject.form.EmailAddress;
import org.jasig.ssp.transferobject.form.EmailStudentRequestForm;
import org.jasig.ssp.transferobject.jobqueue.JobTO;
import org.jasig.ssp.web.api.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.mail.SendFailedException;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@Service
public class PersonEmailServiceImpl implements PersonEmailService {

    private static final Logger OUTER_CLASS_LOGGER = LoggerFactory.getLogger(PersonEmailServiceImpl.class);

    private static final ThreadLocal<Logger> CURRENT_LOGGER = new ThreadLocal<Logger>();

    public static final String BULK_EMAIL_JOB_EXECUTOR_NAME = "bulk-email-executor";
    private static final String BULK_MESSAGES_MAX_MESSAGES_CONFIG_NAME = "mail_bulk_message_limit";
    private static final String BULK_MESSAGES_BATCH_SIZE_CONFIG_NAME = "mail_bulk_message_batch_size";
    private static final String BULK_MESSAGES_MAX_DLQ_SIZE_CONFIG_NAME = "mail_bulk_message_max_dlq_size";
    private static final String BULK_MESSAGES_FAIL_ON_DLQ_OVERFLOW_CONFIG_NAME = "mail_bulk_message_fail_on_dlq_overflow";

    // Careful when changing these messages; might be code looking at them to figure out what happened when
    // a ValidationException occurs
    private static final String MISSING_ANY_VALID_DELIVERY_ADDR_ERROR_MSG = "At least one valid email address must be provided.";
    private static final String MISSING_NON_CC_DELIVERY_ADDR_ERROR_MSG = "At least one valid non-CC email address must be provided.";
    private static final String MISSING_SUBJECT_ERROR_MSG = "Email subject must be provided";
    private static final String MISSING_BODY_ERROR_MSG = "Email body must be provided";
    private static final String MESSAGE_ID_CREATED_FIELD_NAME = "messageId";
    private static final String JOURNAL_ENTRY_ID_CREATED_FIELD_NAME = "journalEntryId";

    @Autowired
    private transient MessageService messageService;

    @Autowired
    private transient JournalEntryService journalEntryService;

    @Autowired
    private transient JournalSourceService journalSourceService;

    @Autowired
    private transient ConfidentialityLevelService confidentialityLevelService;

    @Autowired
    private transient ConfigService configService;

    @Autowired
    private transient JobService jobService;

    @Autowired
    private transient PlatformTransactionManager transactionManager;

    @Autowired
    private transient SecurityService securityService;

    @Autowired
    private transient PersonSearchRequestTOFactory personSearchRequestFactory;

    @Autowired
    private transient PersonSearchService personSearchService;

    @Autowired
    private transient PersonService personService;

    @Autowired
    private transient MessageTemplateService messageTemplateService;

    @Autowired
    private transient VelocityTemplateService velocityTemplateService;

    private static class BulkEmailJobExecutionState extends BasePersonSearchBasedJobExecutionState {
        public int emailSentCount;
        public int journalEntriesCreatedCount;
    }

    private AbstractPersonSearchBasedJobExecutor<BulkEmailJobSpec, BulkEmailJobExecutionState> bulkEmailJobExecutor;

    private AbstractPersonSearchBasedJobQueuer<BulkEmailStudentRequestForm, BulkEmailJobSpec> bulkEmailJobQueuer;

    @Override
    @Transactional
    public Map<String, UUID> emailStudent(EmailStudentRequestForm emailRequest)
            throws ObjectNotFoundException, ValidationException {
        return doEmailStudent(emailRequest, EmailVolume.SINGLE, null);
    }

    private Map<String, UUID> doEmailStudent(EmailStudentRequestForm emailRequest,
            EmailVolume originalRequestVolume, ImmutablePersonIdentifiersTO studentIds)
            throws ObjectNotFoundException, ValidationException {
        boolean sendMessage = true;
        try {
            validateSingleEmailInput(emailRequest, originalRequestVolume);
        } catch (ValidationException e) {
            // fatal unless you're in bulk mode and we're just missing non-cc delivery addrs; in that case we
            // still want to try to generate a journal entry
            if (originalRequestVolume != EmailVolume.BULK
                    || !(MISSING_NON_CC_DELIVERY_ADDR_ERROR_MSG.equals(e.getMessage()))) {
                throw e;
            } else {
                getCurrentLogger().debug("Skipping email message creation for person [{}] because their record"
                        + " lacks the necessary email delivery address/es. This will not necessarily prevent Journal"
                        + " Entry creation.",
                        (emailRequest.getStudentId() == null ? studentIds : emailRequest.getStudentId()));
                sendMessage = false;
            }
        }

        final Map<String, UUID> rslt = Maps.newLinkedHashMap();
        final boolean createJournalEntry = shouldCreateJournalEntryFor(emailRequest, originalRequestVolume);
        final boolean buildMessage = createJournalEntry || sendMessage;

        final Message message = buildMessage ? buildStudentEmail(emailRequest, originalRequestVolume, sendMessage)
                : null;
        rslt.put(MESSAGE_ID_CREATED_FIELD_NAME, message == null ? null : message.getId());

        if (createJournalEntry) {
            final JournalEntry journalEntry = buildJournalEntry(emailRequest, message, originalRequestVolume);
            rslt.put(JOURNAL_ENTRY_ID_CREATED_FIELD_NAME, journalEntry == null ? null : journalEntry.getId());
        } else {
            getCurrentLogger().debug("Skipping Journal Entry creation for person [{}] either because they have no"
                    + " operational record or because the original messaging request declined Journal Entry creation.",
                    (emailRequest.getStudentId() == null ? studentIds : emailRequest.getStudentId()));
        }

        return rslt;
    }

    @Override
    @Transactional(rollbackFor = { ObjectNotFoundException.class, IOException.class, ValidationException.class })
    public JobTO emailStudentsInBulk(BulkEmailStudentRequestForm emailRequest)
            throws ObjectNotFoundException, IOException, ValidationException, SecurityException {
        return bulkEmailJobQueuer.enqueueJob(emailRequest);
    }

    /**
     * Intentionally private b/c we do not want this to participate in this service's "transactional-by-default"
     * public interface. Its transaction is managed by the {@code JobExecutor} assumed to be invoking it.
     *
     */
    private Map<String, ?> sendSingleBulkEmail(ImmutablePersonIdentifiersTO deliveryTargetIds,
            BulkEmailStudentRequestForm spec) throws ObjectNotFoundException, ValidationException {
        final Person student;
        if (deliveryTargetIds.getId() != null) {
            student = personService.get(deliveryTargetIds.getId());
            if (student == null) {
                throw new ObjectNotFoundException(deliveryTargetIds.getId(), Person.class.getName());
            }
        } else {
            student = personService.getBySchoolId(deliveryTargetIds.getSchoolId(), false);
            if (student == null) {
                throw new ObjectNotFoundException(deliveryTargetIds.getSchoolId(), Person.class.getName());
            }
        }
        EmailStudentRequestForm emailStudentRequestForm = new EmailStudentRequestForm(spec, student);
        return doEmailStudent(emailStudentRequestForm, EmailVolume.BULK, deliveryTargetIds);
    }

    private boolean shouldCreateJournalEntryFor(EmailStudentRequestForm emailRequest,
            EmailVolume originalRequestVolume) {
        return emailRequest.getCreateJournalEntry() && emailRequest.getStudentId() != null;
    }

    private String buildJournalEntryCommentFromEmail(EmailStudentRequestForm emailRequest, Message message)
            throws ObjectNotFoundException {

        // Not going to use this for a while, but might as well look it up first to make sure it exists
        // before doing any other work
        final MessageTemplate messageTemplate = messageTemplateService.get(MessageTemplate.EMAIL_JOURNAL_ENTRY_ID);

        final MessageTO messageTO = new MessageTO(message);

        // "originalBody" is definitely a hack. see notes in formatEmailBody. Basically trying to make sure
        // the template here has access to the originally requested, i.e. 'raw', email body, which should be
        // an HTML fragment rather than a full HTML doc. We can't actually guarantee that, though. This is
        // just a best effort for the common case where our UI gives us HTML fragments. We're not putting the
        // entire emailRequest in the render params b/c we don't want to tightly couple that API to the
        // UI API that object currently supports. And there is just one additional field we need at the moment,
        // in addition to the MessageTO, so just using a simple MAP. Really struggled coming up with a good name
        // for it, though. Thus the very generic 'messageContext'.
        final LinkedHashMap<Object, Object> messageContextTO = Maps.newLinkedHashMap();
        messageContextTO.put("originalBody", emailRequest.getEmailBody());

        Map<String, Object> templateParameters = new HashMap<String, Object>();
        templateParameters.put("message", messageTO);
        templateParameters.put("messageContext", messageContextTO);
        return velocityTemplateService.generateContentFromTemplate(messageTemplate.getBody(),
                messageTemplate.bodyTemplateId(), templateParameters);
    }

    private JournalEntry buildJournalEntry(EmailStudentRequestForm emailRequest, Message message,
            EmailVolume originalRequestVolume) throws ObjectNotFoundException, ValidationException {
        Person student = personService.get(emailRequest.getStudentId());

        JournalEntry journalEntry = new JournalEntry();
        journalEntry.setPerson(student);

        String commentFromEmail = buildJournalEntryCommentFromEmail(emailRequest, message);

        ConfidentialityLevel confidentialityLevel;
        if (emailRequest.getConfidentialityLevelId() == null) {
            confidentialityLevel = confidentialityLevelService
                    .get(ConfidentialityLevel.CONFIDENTIALITYLEVEL_EVERYONE);
        } else {
            confidentialityLevel = confidentialityLevelService.get(emailRequest.getConfidentialityLevelId());
        }
        journalEntry.setConfidentialityLevel(confidentialityLevel);
        journalEntry.setComment(commentFromEmail);

        journalEntry.setEntryDate(new Date());
        journalEntry.setJournalSource(journalSourceService.get(JournalSource.JOURNALSOURCE_EMAIL_ID));
        journalEntry = journalEntryService.save(journalEntry);
        return journalEntry;
    }

    private Message buildStudentEmail(EmailStudentRequestForm emailRequest, EmailVolume originalRequestVolume,
            boolean andSend) throws ObjectNotFoundException {
        final EmailAddress addresses = emailRequest.getValidDeliveryAddresses(true);
        final String body = formatEmailBody(emailRequest);
        final SubjectAndBody subjectAndBody = new SubjectAndBody(emailRequest.getEmailSubject(), body);
        return andSend ? messageService.createMessage(addresses.getTo(), addresses.getCc(), subjectAndBody)
                : messageService.createMessageNoSave(addresses.getTo(), addresses.getCc(), subjectAndBody);
    }

    private String formatEmailBody(EmailStudentRequestForm emailRequest) {
        // Yes, a hack. Normally email body requests won't be fully formed HTML docs, so we focus on trying to fix up
        // that case. If someone does send a fully formed HTML doc, we leave it alone, but this *will* cause
        // downstream problems in journal, which does expects HTML fragments not docs.
        final String firstFewChars = emailRequest.getEmailBody()
                .substring(0, Math.min(25, emailRequest.getEmailBody().length())).toLowerCase();
        if (!(firstFewChars.startsWith("<html")) && !(firstFewChars.startsWith("<!doctype"))) {
            return new StringBuilder("<html><body>").append(emailRequest.getEmailBody()).append("</body></html>")
                    .toString();
        }
        return emailRequest.getEmailBody();
    }

    /**
     * When a message in a bulk email batch is sent, the spec is first translated from a
     * {@link BulkEmailStudentRequestForm} into a {@link EmailStudentRequestForm}, which
     * then passes through this validation mechanism. But the rules are different when considering a single-
     * vs bulk message spec. For bulk messages we never want to send a message to the CC *only*, but this
     * is not necessarily a fatal problem for the entire bulk messaging unit of work for that specific delivery
     * target, e.g. might still want to create a Journal Entry.
     *
     * @param emailRequest
     * @param originalRequestVolume
     * @throws ValidationException
     */
    private void validateSingleEmailInput(EmailStudentRequestForm emailRequest, EmailVolume originalRequestVolume)
            throws ValidationException {
        StringBuilder validationMsg = new StringBuilder();
        String EOL = System.getProperty("line.separator");

        // Removed a historical validation that required a studentId (UUID) on emailRequest. We don't actually need that
        // and we can't require it since we now reuse EmailStudentRequestForm when sending bulk email, which may
        // target external-only students, i.e. persons without UUIDs.
        //
        // Also, as hinted at in class comments and a comment attached to these MSG constants... be careful when
        // changing how error messages are emitted... there might be code looking at them to try to figure out
        // what failed to validate.

        if (!emailRequest.hasEmailSubject()) {
            validationMsg.append(MISSING_SUBJECT_ERROR_MSG).append(EOL);
        }
        if (!emailRequest.hasEmailBody()) {
            validationMsg.append(MISSING_BODY_ERROR_MSG).append(EOL);
        }

        if (originalRequestVolume == null) {
            throw new IllegalArgumentException("Must specify an EmailVolume"); // programmer error
        }

        if (originalRequestVolume == EmailVolume.SINGLE && !emailRequest.hasValidDeliveryAddresses()) {
            validationMsg.append(MISSING_ANY_VALID_DELIVERY_ADDR_ERROR_MSG).append(EOL);
        } else if (originalRequestVolume == EmailVolume.BULK && !emailRequest.hasValidNonCcDeliveryAddress()) {
            validationMsg.append(MISSING_NON_CC_DELIVERY_ADDR_ERROR_MSG);
        }

        String validation = validationMsg.toString();
        if (org.apache.commons.lang.StringUtils.isNotBlank(validation)) {
            throw new ValidationException(validation);
        }
    }

    private void validateBulkEmailInput(BulkEmailStudentRequestForm emailRequest) throws ValidationException {
        StringBuilder validationMsg = new StringBuilder();
        String EOL = System.getProperty("line.separator");
        if (!emailRequest.hasEmailSubject()) {
            validationMsg.append("Email subject must be provided").append(EOL);
        }
        if (!emailRequest.hasEmailBody()) {
            validationMsg.append("Email body must be provided").append(EOL);
        }
        if (!emailRequest.hasNonCcDeliveryAddress()) {
            validationMsg.append("Non-cc email delivery addresses must be provided").append(EOL);
        }
        String validation = validationMsg.toString();
        if (org.apache.commons.lang.StringUtils.isNotBlank(validation)) {
            throw new ValidationException(validation);
        }
    }

    @Override
    @Transactional
    public void sendCoachingAssignmentChangeEmail(Person model, UUID oldCoachId)
            throws ObjectNotFoundException, SendFailedException, ValidationException {

        if (oldCoachId == null || model.getCoach() == null || !model.getCoach().hasEmailAddresses())
            return;
        Person oldCoach = personService.get(oldCoachId);
        String appTitle = configService.getByNameEmpty("app_title");
        String serverExternalPath = configService.getByNameEmpty("serverExternalPath");

        String message = oldCoach.getFullName() + " has assigned " + model.getFullName() + " to your caseload in "
                + appTitle + ". Please visit " + serverExternalPath + " to view the student's information in "
                + appTitle + ".";
        String subject = "A coaching assignment has changed in " + appTitle;

        SubjectAndBody subjectAndBody = new SubjectAndBody(subject, message);
        if (oldCoach.hasEmailAddresses() && model.getWatcherEmailAddresses().isEmpty()) {
            messageService.createMessage(model.getCoach(),
                    StringUtils.arrayToCommaDelimitedString(
                            oldCoach.getEmailAddresses().toArray(new String[oldCoach.getEmailAddresses().size()])),
                    subjectAndBody);
        } else if (oldCoach.hasEmailAddresses() && !model.getWatcherEmailAddresses().isEmpty()) {
            Set<String> emails = new HashSet<String>();
            emails.addAll(oldCoach.getEmailAddresses());
            emails.addAll(model.getWatcherEmailAddresses());
            messageService.createMessage(model.getCoach(),
                    StringUtils.arrayToCommaDelimitedString(emails.toArray(new String[emails.size()])),
                    subjectAndBody);
        } else if (!oldCoach.hasEmailAddresses() && model.getWatcherEmailAddresses().isEmpty()) {
            messageService.createMessage(model.getCoach(), StringUtils.arrayToCommaDelimitedString(
                    model.getWatcherEmailAddresses().toArray(new String[model.getWatcherEmailAddresses().size()])),
                    subjectAndBody);
        } else {
            messageService.createMessage(model.getCoach(), "", subjectAndBody);
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initBulkEmailJobExecutor();
        initBulkEmailJobQueuer();
    }

    private void initBulkEmailJobExecutor() {

        this.bulkEmailJobExecutor = new AbstractPersonSearchBasedJobExecutor<BulkEmailJobSpec, BulkEmailJobExecutionState>(
                BULK_EMAIL_JOB_EXECUTOR_NAME, jobService, transactionManager, null, personSearchService,
                personSearchRequestFactory, configService) {

            private final Logger logger = LoggerFactory
                    .getLogger(PersonEmailServiceImpl.this.getClass().getName() + ".BulkEmailJobExecutor");

            /**
             * The actual 'important' override... all the rest of the overrides are mostly boilerplate.
             */
            @Override
            protected Map<String, ?> executeForSinglePerson(ImmutablePersonIdentifiersTO personIds,
                    BulkEmailJobSpec executionSpec, BulkEmailJobExecutionState executionState, UUID jobId)
                    throws ValidationException, ObjectNotFoundException {
                return sendSingleBulkEmail(personIds, executionSpec.getCoreSpec());
            }

            @Override
            protected JobExecutionResult<BulkEmailJobExecutionState> executeJobDeserialized(
                    BulkEmailJobSpec executionSpec, BulkEmailJobExecutionState executionState, UUID jobId) {
                // TODO this copy pasted all over in these executor subclasses... abstract somehow
                try {
                    PersonEmailServiceImpl.this.setCurrentLogger(logger);
                    return super.executeJobDeserialized(executionSpec, executionState, jobId);
                } finally {
                    PersonEmailServiceImpl.this.setCurrentLogger(null);
                }
            }

            @Override
            protected void recordSuccessful(ImmutablePersonIdentifiersTO personIds, Map<String, ?> results,
                    BulkEmailJobSpec executionSpec, BulkEmailJobExecutionState executionState, UUID jobId) {
                super.recordSuccessful(personIds, results, executionSpec, executionState, jobId);
                if (results.get(MESSAGE_ID_CREATED_FIELD_NAME) != null) {
                    executionState.emailSentCount++;
                }
                if (results.get(JOURNAL_ENTRY_ID_CREATED_FIELD_NAME) != null) {
                    executionState.journalEntriesCreatedCount++;
                }
            }

            @Override
            protected String decorateProcessingCompleteLogMessage(String baseMsg,
                    BulkEmailJobExecutionState executionState) {
                return new StringBuilder(baseMsg).append(" Emails sent: [").append(executionState.emailSentCount)
                        .append("]. Journal entries created: [").append(executionState.journalEntriesCreatedCount)
                        .append("]").toString();
            }

            @Override
            protected BulkEmailJobExecutionState newJobExecutionState() {
                return new BulkEmailJobExecutionState();
            }

            @Override
            protected BulkEmailJobSpec deserializeJobSpecWithCheckedExceptions(String jobSpecStr) throws Exception {
                return getObjectMapper().readValue(jobSpecStr, BulkEmailJobSpec.class);
            }

            @Override
            protected BulkEmailJobExecutionState deserializeJobStateWithCheckedExceptions(String jobStateStr)
                    throws Exception {
                return getObjectMapper().readValue(jobStateStr, BulkEmailJobExecutionState.class);
            }

            @Override
            protected Logger getCurrentLogger() {
                return PersonEmailServiceImpl.this.getCurrentLogger();
            }

            @Override
            protected String getPageSizeConfigName() {
                return BULK_MESSAGES_BATCH_SIZE_CONFIG_NAME;
            }

            @Override
            protected String getDlqSizeConfigName() {
                return BULK_MESSAGES_MAX_DLQ_SIZE_CONFIG_NAME;
            }

            @Override
            protected String getFailOnSlqOverflowConfigName() {
                return BULK_MESSAGES_FAIL_ON_DLQ_OVERFLOW_CONFIG_NAME;
            }
        };

        this.jobService.registerJobExecutor(this.bulkEmailJobExecutor);
    }

    private void initBulkEmailJobQueuer() {
        if (this.bulkEmailJobExecutor == null) {
            // programmer error
            throw new IllegalStateException("Bulk email JobExecutor not yet initialized");
        }
        bulkEmailJobQueuer = new AbstractPersonSearchBasedJobQueuer<BulkEmailStudentRequestForm, BulkEmailJobSpec>(
                this.bulkEmailJobExecutor, securityService, personSearchRequestFactory, personSearchService) {
            @Override
            protected BulkEmailStudentRequestForm validateJobRequest(BulkEmailStudentRequestForm jobRequest)
                    throws ValidationException {
                validateBulkEmailInput(jobRequest);
                return jobRequest;
            }

            @Override
            protected void validatePersonSearchResults(long searchResultCount, PersonSearchRequest searchRequest,
                    Person currentSspPerson) throws ValidationException {
                super.validatePersonSearchResults(searchResultCount, searchRequest, currentSspPerson);
                final int maxMessages = configService
                        .getByNameExceptionOrDefaultAsInt(BULK_MESSAGES_MAX_MESSAGES_CONFIG_NAME);
                if (maxMessages > 0 && searchResultCount > maxMessages) {
                    throw new ValidationException("Too many person search results: " + searchResultCount
                            + ". Limit: " + maxMessages + ". Can't send message.");
                }
            }

            @Override
            protected BulkEmailJobSpec newJobSpec(BulkEmailStudentRequestForm jobRequest, Person currentSspPerson,
                    PersonSearchRequest searchRequest, long searchResultCount) {
                return new BulkEmailJobSpec(jobRequest);
            }
        };
    }

    private Logger getCurrentLogger() {
        return CURRENT_LOGGER.get() == null ? OUTER_CLASS_LOGGER : CURRENT_LOGGER.get();
    }

    private void setCurrentLogger(Logger logger) {
        CURRENT_LOGGER.set(logger);
    }

    private static enum EmailVolume {
        SINGLE, BULK
    }
}