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

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.EarlyAlertServiceImpl.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; // NOPMD by jon.adams

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.mail.SendFailedException;
import javax.validation.constraints.NotNull;

import org.apache.commons.lang.StringUtils;
import org.jasig.ssp.config.EarlyAlertResponseReminderRecipientsConfig;
import org.jasig.ssp.dao.EarlyAlertDao;
import org.jasig.ssp.factory.EarlyAlertSearchResultTOFactory;
import org.jasig.ssp.model.EarlyAlert;
import org.jasig.ssp.model.EarlyAlertRouting;
import org.jasig.ssp.model.EarlyAlertSearchResult;
import org.jasig.ssp.model.Message;
import org.jasig.ssp.model.ObjectStatus;
import org.jasig.ssp.model.Person;
import org.jasig.ssp.model.PersonProgramStatus;
import org.jasig.ssp.model.SubjectAndBody;
import org.jasig.ssp.model.WatchStudent;
import org.jasig.ssp.model.external.FacultyCourse;
import org.jasig.ssp.model.external.Term;
import org.jasig.ssp.model.reference.Campus;
import org.jasig.ssp.model.reference.EarlyAlertReason;
import org.jasig.ssp.model.reference.EarlyAlertSuggestion;
import org.jasig.ssp.model.reference.ProgramStatus;
import org.jasig.ssp.model.reference.StudentType;
import org.jasig.ssp.security.SspUser;
import org.jasig.ssp.service.AbstractPersonAssocAuditableService;
import org.jasig.ssp.service.EarlyAlertRoutingService;
import org.jasig.ssp.service.EarlyAlertService;
import org.jasig.ssp.service.MessageService;
import org.jasig.ssp.service.ObjectNotFoundException;
import org.jasig.ssp.service.PersonProgramStatusService;
import org.jasig.ssp.service.PersonService;
import org.jasig.ssp.service.SecurityService;
import org.jasig.ssp.service.external.FacultyCourseService;
import org.jasig.ssp.service.external.TermService;
import org.jasig.ssp.service.reference.ConfigService;
import org.jasig.ssp.service.reference.EarlyAlertReasonService;
import org.jasig.ssp.service.reference.EarlyAlertSuggestionService;
import org.jasig.ssp.service.reference.MessageTemplateService;
import org.jasig.ssp.service.reference.ProgramStatusService;
import org.jasig.ssp.service.reference.StudentTypeService;
import org.jasig.ssp.transferobject.EarlyAlertSearchResultTO;
import org.jasig.ssp.transferobject.EarlyAlertTO;
import org.jasig.ssp.transferobject.PagedResponse;
import org.jasig.ssp.transferobject.form.EarlyAlertSearchForm;
import org.jasig.ssp.transferobject.messagetemplate.CoachPersonLiteMessageTemplateTO;
import org.jasig.ssp.transferobject.messagetemplate.EarlyAlertMessageTemplateTO;
import org.jasig.ssp.transferobject.reports.EarlyAlertCourseCountsTO;
import org.jasig.ssp.transferobject.reports.EarlyAlertReasonCountsTO;
import org.jasig.ssp.transferobject.reports.EarlyAlertStudentReportTO;
import org.jasig.ssp.transferobject.reports.EarlyAlertStudentSearchTO;
import org.jasig.ssp.transferobject.reports.EntityCountByCoachSearchForm;
import org.jasig.ssp.transferobject.reports.EntityStudentCountByCoachTO;
import org.jasig.ssp.util.DateTimeUtils;
import org.jasig.ssp.util.collections.Pair;
import org.jasig.ssp.util.collections.Triple;
import org.jasig.ssp.util.sort.PagingWrapper;
import org.jasig.ssp.util.sort.SortingAndPaging;
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.annotation.Transactional;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * EarlyAlert service implementation
 * 
 * @author jon.adams
 * 
 */
@Service
@Transactional
public class EarlyAlertServiceImpl extends // NOPMD
        AbstractPersonAssocAuditableService<EarlyAlert> implements EarlyAlertService {

    @Autowired
    private transient EarlyAlertDao dao;
    @Autowired
    private transient ConfigService configService;
    @Autowired
    private transient EarlyAlertRoutingService earlyAlertRoutingService;
    @Autowired
    private transient MessageService messageService;
    @Autowired
    private transient MessageTemplateService messageTemplateService;
    @Autowired
    private transient EarlyAlertReasonService earlyAlertReasonService;
    @Autowired
    private transient EarlyAlertSuggestionService earlyAlertSuggestionService;
    @Autowired
    private transient PersonService personService;
    @Autowired
    private transient FacultyCourseService facultyCourseService;
    @Autowired
    private transient TermService termService;
    @Autowired
    private transient PersonProgramStatusService personProgramStatusService;
    @Autowired
    private transient ProgramStatusService programStatusService;
    @Autowired
    private transient StudentTypeService studentTypeService;
    @Autowired
    private transient SecurityService securityService;
    @Autowired
    private transient EarlyAlertSearchResultTOFactory searchResultFactory;
    @Autowired
    private transient EarlyAlertResponseReminderRecipientsConfig earReminderRecipientConfig;

    private static final Logger LOGGER = LoggerFactory.getLogger(EarlyAlertServiceImpl.class);

    @Override
    protected EarlyAlertDao getDao() {
        return dao;
    }

    @Override
    @Transactional(rollbackFor = { ObjectNotFoundException.class, ValidationException.class })
    public EarlyAlert create(@NotNull final EarlyAlert earlyAlert)
            throws ObjectNotFoundException, ValidationException {
        // Validate objects
        if (earlyAlert == null) {
            throw new IllegalArgumentException("EarlyAlert must be provided.");
        }

        if (earlyAlert.getPerson() == null) {
            throw new ValidationException("EarlyAlert Student data must be provided.");
        }

        final Person student = earlyAlert.getPerson();

        // Figure student advisor or early alert coordinator
        final UUID assignedAdvisor = getEarlyAlertAdvisor(earlyAlert);
        if (assignedAdvisor == null) {
            throw new ValidationException(
                    "Could not determine the Early Alert Advisor for student ID " + student.getId());
        }

        if (student.getCoach() == null || assignedAdvisor.equals(student.getCoach().getId())) {
            student.setCoach(personService.get(assignedAdvisor));
        }

        ensureValidAlertedOnPersonStateNoFail(student);

        // Create alert
        final EarlyAlert saved = getDao().save(earlyAlert);

        // Send e-mail to assigned advisor (coach)
        try {
            sendMessageToAdvisor(saved, earlyAlert.getEmailCC());
        } catch (final SendFailedException e) {
            LOGGER.warn("Could not send Early Alert message to advisor.", e);
            throw new ValidationException(
                    "Early Alert notification e-mail could not be sent to advisor. Early Alert was NOT created.",
                    e);
        }

        // Send e-mail CONFIRMATION to faculty
        try {
            sendConfirmationMessageToFaculty(saved);
        } catch (final SendFailedException e) {
            LOGGER.warn("Could not send Early Alert confirmation to faculty.", e);
            throw new ValidationException(
                    "Early Alert confirmation e-mail could not be sent. Early Alert was NOT created.", e);
        }

        return saved;
    }

    @Override
    public void closeEarlyAlert(UUID earlyAlertId) throws ObjectNotFoundException, ValidationException {
        final EarlyAlert earlyAlert = getDao().get(earlyAlertId);

        // DAOs don't implement ObjectNotFoundException consistently and we'd
        // rather they not implement it at all, so a small attempt at 'future
        // proofing' here
        if (earlyAlert == null) {
            throw new ObjectNotFoundException(earlyAlertId, EarlyAlert.class.getName());
        }

        if (earlyAlert.getClosedDate() != null) {
            // already closed
            return;
        }

        final SspUser sspUser = securityService.currentUser();
        if (sspUser == null) {
            throw new ValidationException("Early Alert cannot be closed by a null User.");
        }

        earlyAlert.setClosedDate(new Date());
        earlyAlert.setClosedBy(sspUser.getPerson());

        // This save will result in a Hib session flush, which works fine with
        // our current usage. Future use cases might prefer to delay the
        // flush and we can address that when the time comes. Might not even
        // need to change anything here if it turns out nothing actually
        // *depends* on the flush.
        getDao().save(earlyAlert);
    }

    @Override
    public void openEarlyAlert(UUID earlyAlertId) throws ObjectNotFoundException, ValidationException {
        final EarlyAlert earlyAlert = getDao().get(earlyAlertId);

        // DAOs don't implement ObjectNotFoundException consistently and we'd
        // rather they not implement it at all, so a small attempt at 'future
        // proofing' here
        if (earlyAlert == null) {
            throw new ObjectNotFoundException(earlyAlertId, EarlyAlert.class.getName());
        }

        if (earlyAlert.getClosedDate() == null) {
            return;
        }

        final SspUser sspUser = securityService.currentUser();
        if (sspUser == null) {
            throw new ValidationException("Early Alert cannot be closed by a null User.");
        }

        earlyAlert.setClosedDate(null);
        earlyAlert.setClosedBy(null);

        // This save will result in a Hib session flush, which works fine with
        // our current usage. Future use cases might prefer to delay the
        // flush and we can address that when the time comes. Might not even
        // need to change anything here if it turns out nothing actually
        // *depends* on the flush.
        getDao().save(earlyAlert);
    }

    @Override
    public EarlyAlert save(@NotNull final EarlyAlert obj) throws ObjectNotFoundException {
        final EarlyAlert current = getDao().get(obj.getId());

        current.setCourseName(obj.getCourseName());
        current.setCourseTitle(obj.getCourseTitle());
        current.setEmailCC(obj.getEmailCC());
        current.setCampus(obj.getCampus());
        current.setEarlyAlertReasonOtherDescription(obj.getEarlyAlertReasonOtherDescription());
        current.setComment(obj.getComment());
        current.setClosedDate(obj.getClosedDate());
        if (obj.getClosedById() == null) {
            current.setClosedBy(null);
        } else {
            current.setClosedBy(personService.get(obj.getClosedById()));
        }

        if (obj.getPerson() == null) {
            current.setPerson(null);
        } else {
            current.setPerson(personService.get(obj.getPerson().getId()));
        }

        final Set<EarlyAlertReason> earlyAlertReasons = new HashSet<EarlyAlertReason>();
        if (obj.getEarlyAlertReasons() != null) {
            for (final EarlyAlertReason reason : obj.getEarlyAlertReasons()) {
                earlyAlertReasons.add(earlyAlertReasonService.load(reason.getId()));
            }
        }

        current.setEarlyAlertReasons(earlyAlertReasons);

        final Set<EarlyAlertSuggestion> earlyAlertSuggestions = new HashSet<EarlyAlertSuggestion>();
        if (obj.getEarlyAlertSuggestions() != null) {
            for (final EarlyAlertSuggestion reason : obj.getEarlyAlertSuggestions()) {
                earlyAlertSuggestions.add(earlyAlertSuggestionService.load(reason.getId()));
            }
        }

        current.setEarlyAlertSuggestions(earlyAlertSuggestions);

        return getDao().save(current);
    }

    @Override
    public PagingWrapper<EarlyAlert> getAllForPerson(final Person person, final SortingAndPaging sAndP) {
        return getDao().getAllForPersonId(person.getId(), sAndP);
    }

    /**
     * Business logic to determine the advisor that is assigned to the student
     * for this Early Alert.
     * 
     * @param earlyAlert
     *            EarlyAlert instance
     * @throws ValidationException
     *             If Early Alert, Student, and/or system information could not
     *             determine the advisor for this student.
     * @return The assigned advisor
     */
    private UUID getEarlyAlertAdvisor(final EarlyAlert earlyAlert) throws ValidationException {
        // Check for student already assigned to an advisor (a.k.a. coach)
        if ((earlyAlert.getPerson().getCoach() != null) && (earlyAlert.getPerson().getCoach().getId() != null)) {
            return earlyAlert.getPerson().getCoach().getId();
        }

        // Get campus Early Alert coordinator
        if (earlyAlert.getCampus() == null) {
            throw new IllegalArgumentException("Campus ID can not be null.");
        }

        if (earlyAlert.getCampus().getEarlyAlertCoordinatorId() != null) {
            // Return Early Alert coordinator UUID
            return earlyAlert.getCampus().getEarlyAlertCoordinatorId();
        }

        // TODO If no campus EA Coordinator, assign to default EA Coordinator
        // (which is not yet implemented)

        // getEarlyAlertAdvisor should never return null
        throw new ValidationException(
                "Could not determined the Early Alert Coordinator for this student. Ensure that a default coordinator is set globally and for all campuses.");
    }

    private void ensureValidAlertedOnPersonStateNoFail(Person person) {
        try {
            ensureValidAlertedOnPersonStateOrFail(person);
        } catch (Exception e) {
            LOGGER.error(
                    "Unable to set a program status or student type on "
                            + "person '{}'. This is likely to prevent that person "
                            + "record from appearing in caseloads, student searches, " + "and some reports.",
                    person.getId(), e);
        }
    }

    private void ensureValidAlertedOnPersonStateOrFail(Person person)
            throws ObjectNotFoundException, ValidationException {

        if (person.getObjectStatus() != ObjectStatus.ACTIVE) {
            person.setObjectStatus(ObjectStatus.ACTIVE);
        }

        final ProgramStatus programStatus = programStatusService.getActiveStatus();
        if (programStatus == null) {
            throw new ObjectNotFoundException("Unable to find a ProgramStatus representing \"activeness\".",
                    "ProgramStatus");
        }

        Set<PersonProgramStatus> programStatuses = person.getProgramStatuses();
        if (programStatuses == null || programStatuses.isEmpty()) {
            PersonProgramStatus personProgramStatus = new PersonProgramStatus();
            personProgramStatus.setEffectiveDate(new Date());
            personProgramStatus.setProgramStatus(programStatus);
            personProgramStatus.setPerson(person);
            programStatuses.add(personProgramStatus);
            person.setProgramStatuses(programStatuses);
            // save should cascade, but make sure custom create logic fires
            personProgramStatusService.create(personProgramStatus);
        }

        if (person.getStudentType() == null) {
            StudentType studentType = studentTypeService.get(StudentType.EAL_ID);
            if (studentType == null) {
                throw new ObjectNotFoundException(
                        "Unable to find a StudentType representing an early " + "alert-assigned type.",
                        "StudentType");
            }
            person.setStudentType(studentType);
        }
    }

    /**
     * Send e-mail ({@link Message}) to the assigned advisor for the student.
     * 
     * @param earlyAlert
     *            Early Alert
     * @param emailCC
     *            Email address to also CC this message
     * @throws ObjectNotFoundException
     * @throws SendFailedException
     * @throws ValidationException
     */
    private void sendMessageToAdvisor(@NotNull final EarlyAlert earlyAlert, // NOPMD
            final String emailCC) throws ObjectNotFoundException, SendFailedException, ValidationException {
        if (earlyAlert == null) {
            throw new IllegalArgumentException("Early alert was missing.");
        }

        if (earlyAlert.getPerson() == null) {
            throw new IllegalArgumentException("EarlyAlert Person is missing.");
        }

        final Person person = earlyAlert.getPerson().getCoach();
        final SubjectAndBody subjAndBody = messageTemplateService
                .createEarlyAlertAdvisorConfirmationMessage(fillTemplateParameters(earlyAlert));

        Set<String> watcherEmailAddresses = new HashSet<String>(earlyAlert.getPerson().getWatcherEmailAddresses());
        if (emailCC != null && !emailCC.isEmpty()) {
            watcherEmailAddresses.add(emailCC);
        }
        if (person == null) {
            LOGGER.warn(
                    "Student {} had no coach when EarlyAlert {} was" + " created. Unable to send message to coach.",
                    earlyAlert.getPerson(), earlyAlert);
        } else {
            // Create and queue the message
            final Message message = messageService
                    .createMessage(person,
                            org.springframework.util.StringUtils.arrayToCommaDelimitedString(
                                    watcherEmailAddresses.toArray(new String[watcherEmailAddresses.size()])),
                            subjAndBody);
            LOGGER.info("Message {} created for EarlyAlert {}", message, earlyAlert);
        }

        // Send same message to all applicable Campus Early Alert routing
        // entries
        final PagingWrapper<EarlyAlertRouting> routes = earlyAlertRoutingService
                .getAllForCampus(earlyAlert.getCampus(), new SortingAndPaging(ObjectStatus.ACTIVE));
        if (routes.getResults() > 0) {
            for (final EarlyAlertRouting route : routes.getRows()) {
                // Check that route applies
                if (route.getEarlyAlertReason() == null) {
                    throw new ObjectNotFoundException("EarlyAlertRouting missing EarlyAlertReason.",
                            "EarlyAlertReason");
                }

                // Only routes that are for any of the Reasons in this
                // EarlyAlert should be applied.
                if ((earlyAlert.getEarlyAlertReasons() == null)
                        || !earlyAlert.getEarlyAlertReasons().contains(route.getEarlyAlertReason())) {
                    continue;
                }

                // Send e-mail to specific person
                final Person to = route.getPerson();
                if ((to != null) && !StringUtils.isEmpty(to.getPrimaryEmailAddress())) {
                    final Message message = messageService.createMessage(to, null, subjAndBody);
                    LOGGER.info("Message {} for EarlyAlert {} also routed to {}",
                            new Object[] { message, earlyAlert, to }); // NOPMD
                }

                // Send e-mail to a group
                if (!StringUtils.isEmpty(route.getGroupName()) && !StringUtils.isEmpty(route.getGroupEmail())) {
                    final Message message = messageService.createMessage(route.getGroupEmail(), null, subjAndBody);
                    LOGGER.info("Message {} for EarlyAlert {} also routed to {}",
                            new Object[] { message, earlyAlert, // NOPMD
                                    route.getGroupEmail() });
                }
            }
        }
    }

    @Override
    public void sendMessageToStudent(@NotNull final EarlyAlert earlyAlert)
            throws ObjectNotFoundException, SendFailedException, ValidationException {
        if (earlyAlert == null) {
            throw new IllegalArgumentException("EarlyAlert was missing.");
        }

        if (earlyAlert.getPerson() == null) {
            throw new IllegalArgumentException("EarlyAlert.Person is missing.");
        }

        final Person person = earlyAlert.getPerson();
        final SubjectAndBody subjAndBody = messageTemplateService
                .createEarlyAlertToStudentMessage(fillTemplateParameters(earlyAlert));

        Set<String> watcheremails = new HashSet<String>(person.getWatcherEmailAddresses());
        // Create and queue the message
        final Message message = messageService.createMessage(person, org.springframework.util.StringUtils
                .arrayToCommaDelimitedString(watcheremails.toArray(new String[watcheremails.size()])), subjAndBody);

        LOGGER.info("Message {} created for EarlyAlert {}", message, earlyAlert);
    }

    /**
     * Send confirmation e-mail ({@link Message}) to the faculty who created
     * this alert.
     * 
     * @param earlyAlert
     *            Early Alert
     * @throws ObjectNotFoundException
     * @throws SendFailedException
     * @throws ValidationException
     */
    private void sendConfirmationMessageToFaculty(final EarlyAlert earlyAlert)
            throws ObjectNotFoundException, SendFailedException, ValidationException {
        if (earlyAlert == null) {
            throw new IllegalArgumentException("EarlyAlert was missing.");
        }

        if (earlyAlert.getPerson() == null) {
            throw new IllegalArgumentException("EarlyAlert.Person is missing.");
        }

        final UUID personId = earlyAlert.getCreatedBy().getId();
        Person person = personService.get(personId);
        if (person == null) {
            LOGGER.warn("EarlyAlert {} has no creator. Unable to send" + " confirmation message to faculty.",
                    earlyAlert);
        } else {
            final SubjectAndBody subjAndBody = messageTemplateService
                    .createEarlyAlertFacultyConfirmationMessage(fillTemplateParameters(earlyAlert));

            // Create and queue the message
            final Message message = messageService.createMessage(person, null, subjAndBody);

            LOGGER.info("Message {} created for EarlyAlert {}", message, earlyAlert);
        }
    }

    @Override
    public Map<String, Object> fillTemplateParameters(@NotNull final EarlyAlert earlyAlert) {
        if (earlyAlert == null) {
            throw new IllegalArgumentException("EarlyAlert was missing.");
        }

        if (earlyAlert.getPerson() == null) {
            throw new IllegalArgumentException("EarlyAlert.Person is missing.");
        }

        if (earlyAlert.getCreatedBy() == null) {
            throw new IllegalArgumentException("EarlyAlert.CreatedBy is missing.");
        }

        if (earlyAlert.getCampus() == null) {
            throw new IllegalArgumentException("EarlyAlert.Campus is missing.");
        }

        // ensure earlyAlert.createdBy is populated
        if ((earlyAlert.getCreatedBy() == null) || (earlyAlert.getCreatedBy().getFirstName() == null)) {
            if (earlyAlert.getCreatedBy() == null) {
                throw new IllegalArgumentException("EarlyAlert.CreatedBy is missing.");
            }
            //
            //         try {
            //            //earlyAlert.setCreatedBy(new AuditPerson(personService.get(earlyAlert.getCreatedBy().getId())).getId());
            //         } catch (final ObjectNotFoundException e) {
            //            throw new IllegalArgumentException(
            //                  "EarlyAlert.CreatedBy.Id could not be loaded.", e);
            //         }
        }

        final Map<String, Object> templateParameters = Maps.newHashMap();

        final String courseName = earlyAlert.getCourseName();
        if (StringUtils.isNotBlank(courseName)) {
            Person creator;
            try {
                creator = personService.get(earlyAlert.getCreatedBy().getId());
            } catch (ObjectNotFoundException e1) {
                throw new IllegalArgumentException("EarlyAlert.CreatedBy.Id could not be loaded.", e1);
            }
            final String facultySchoolId = creator.getSchoolId();
            if ((StringUtils.isNotBlank(facultySchoolId))) {
                String termCode = earlyAlert.getCourseTermCode();
                FacultyCourse course = null;
                try {
                    if (StringUtils.isBlank(termCode)) {
                        course = facultyCourseService.getCourseByFacultySchoolIdAndFormattedCourse(facultySchoolId,
                                courseName);
                    } else {
                        course = facultyCourseService.getCourseByFacultySchoolIdAndFormattedCourseAndTermCode(
                                facultySchoolId, courseName, termCode);
                    }
                } catch (ObjectNotFoundException e) {
                    // Trace irrelevant. see below for logging. prefer to
                    // do it there, after the null check b/c not all service
                    // methods implement ObjectNotFoundException reliably.
                }
                if (course != null) {
                    templateParameters.put("course", course);
                    if (StringUtils.isBlank(termCode)) {
                        termCode = course.getTermCode();
                    }
                    if (StringUtils.isNotBlank(termCode)) {
                        Term term = null;
                        try {
                            term = termService.getByCode(termCode);
                        } catch (ObjectNotFoundException e) {
                            // Trace irrelevant. See below for logging.
                        }
                        if (term != null) {
                            templateParameters.put("term", term);
                        } else {
                            LOGGER.info(
                                    "Not adding term to message template" + " params or early alert {} because"
                                            + " the term code {} did not resolve to" + " an external term record",
                                    earlyAlert.getId(), termCode);
                        }
                    }
                } else {
                    LOGGER.info(
                            "Not adding course nor term to message template"
                                    + " params for early alert {} because the associated"
                                    + " course {} and faculty school id {} did not"
                                    + " resolve to an external course record.",
                            new Object[] { earlyAlert.getId(), courseName, facultySchoolId });
                }
            }
        }
        Person creator = null;
        try {
            creator = personService.get(earlyAlert.getCreatedBy().getId());
        } catch (ObjectNotFoundException exp) {
            LOGGER.error("Early Alert Creator Not found sending message for early alert:" + earlyAlert.getId(),
                    exp);
        }

        EarlyAlertMessageTemplateTO eaMTO = new EarlyAlertMessageTemplateTO(earlyAlert, creator);

        //Only early alerts response late messages sent to coaches
        if (eaMTO.getCoach() == null) {
            try {
                // if no earlyAlert.getCampus()  error thrown by design, should never not be a campus.
                eaMTO.setCoach(new CoachPersonLiteMessageTemplateTO(
                        personService.get(earlyAlert.getCampus().getEarlyAlertCoordinatorId())));
            } catch (ObjectNotFoundException exp) {
                LOGGER.error("Early Alert with id: " + earlyAlert.getId()
                        + " does not have valid campus coordinator, no coach assigned: "
                        + earlyAlert.getCampus().getEarlyAlertCoordinatorId(), exp);
            }
        }

        templateParameters.put("earlyAlert", eaMTO);
        templateParameters.put("termToRepresentEarlyAlert",
                configService.getByNameEmpty("term_to_represent_early_alert"));
        templateParameters.put("TermToRepresentEarlyAlert",
                configService.getByNameEmpty("term_to_represent_early_alert"));
        templateParameters.put("linkToSSP", configService.getByNameEmpty("serverExternalPath"));
        templateParameters.put("applicationTitle", configService.getByNameEmpty("app_title"));
        templateParameters.put("institutionName", configService.getByNameEmpty("inst_name"));

        templateParameters.put("FirstName", eaMTO.getPerson().getFirstName());
        templateParameters.put("LastName", eaMTO.getPerson().getLastName());
        templateParameters.put("CourseName", eaMTO.getCourseName());

        return templateParameters;
    }

    @Override
    public void applyEarlyAlertCounts(Person person) {
        if (person == null) {
            return; // can occur in some legit person lookup call paths
        }
        //      Map<UUID,Number> activeCnts =
        //            getCountOfActiveAlertsForPeopleIds(Sets.newHashSet(person.getId()));
        //      if ( activeCnts == null || !(activeCnts.containsKey(person.getId())) ) {
        //         person.setActiveAlertsCount(0);
        //      } else {
        //         person.setActiveAlertsCount(activeCnts.get(person.getId()));
        //      }
        //      Map<UUID,Number> closedCnts =
        //            getCountOfClosedAlertsForPeopleIds(Sets.newHashSet(person.getId()));
        //      if ( closedCnts == null || !(closedCnts.containsKey(person.getId())) ) {
        //         person.setClosedAlertsCount(0);
        //      } else {
        //         person.setClosedAlertsCount(closedCnts.get(person.getId()));
        //      }
    }

    @Override
    public Map<UUID, Number> getCountOfActiveAlertsForPeopleIds(final Collection<UUID> peopleIds) {
        return dao.getCountOfActiveAlertsForPeopleIds(peopleIds);
    }

    @Override
    public Map<UUID, Number> getCountOfClosedAlertsForPeopleIds(final Collection<UUID> peopleIds) {
        return dao.getCountOfClosedAlertsForPeopleIds(peopleIds);
    }

    @Override
    public Long getCountOfEarlyAlertsForSchoolIds(final Collection<String> schoolIds, Campus campus) {
        return dao.getCountOfAlertsForSchoolIds(schoolIds, campus);
    }

    @Override
    public Long getEarlyAlertCountForCoach(Person coach, Date createDateFrom, Date createDateTo,
            List<UUID> studentTypeIds) {
        return dao.getEarlyAlertCountForCoach(coach, createDateFrom, createDateTo, studentTypeIds);
    }

    @Override
    public Long getStudentEarlyAlertCountForCoach(Person coach, Date createDateFrom, Date createDateTo,
            List<UUID> studentTypeIds) {
        return dao.getStudentEarlyAlertCountForCoach(coach, createDateFrom, createDateTo, studentTypeIds);
    }

    @Override
    public Long getEarlyAlertCountForCreatedDateRange(String termCode, Date createDatedFrom, Date createdDateTo,
            Campus campus, String rosterStatus) {
        return dao.getEarlyAlertCountForCreatedDateRange(termCode, createDatedFrom, createdDateTo, campus,
                rosterStatus);
    }

    @Override
    public Long getClosedEarlyAlertCountForClosedDateRange(Date closedDateFrom, Date closedDateTo, Campus campus,
            String rosterStatus) {
        return dao.getClosedEarlyAlertCountForClosedDateRange(closedDateFrom, closedDateTo, campus, rosterStatus);
    }

    @Override
    public Long getClosedEarlyAlertsCountForEarlyAlertCreatedDateRange(String termCode, Date createDatedFrom,
            Date createdDateTo, Campus campus, String rosterStatus) {
        return dao.getClosedEarlyAlertsCountForEarlyAlertCreatedDateRange(termCode, createDatedFrom, createdDateTo,
                campus, rosterStatus);
    }

    @Override
    public Long getStudentCountForEarlyAlertCreatedDateRange(String termCode, Date createDatedFrom,
            Date createdDateTo, Campus campus, String rosterStatus) {
        return dao.getStudentCountForEarlyAlertCreatedDateRange(termCode, createDatedFrom, createdDateTo, campus,
                rosterStatus);
    }

    @Override
    public PagingWrapper<EarlyAlertStudentReportTO> getStudentsEarlyAlertCountSetForCriteria(
            EarlyAlertStudentSearchTO earlyAlertStudentSearchTO, SortingAndPaging createForSingleSort) {
        return dao.getStudentsEarlyAlertCountSetForCriteria(earlyAlertStudentSearchTO, createForSingleSort);
    }

    @Override
    public List<EarlyAlertCourseCountsTO> getStudentEarlyAlertCountSetPerCourses(String termCode,
            Date createdDateFrom, Date createdDateTo, Campus campus, ObjectStatus objectStatus) {
        return dao.getStudentEarlyAlertCountSetPerCourses(termCode, createdDateFrom, createdDateTo, campus,
                objectStatus);
    }

    @Override
    public List<Triple<String, Long, Long>> getEarlyAlertReasonTypeCountByCriteria(Campus campus, String termCode,
            Date createdDateFrom, Date createdDateTo, ObjectStatus status) {
        return dao.getEarlyAlertReasonTypeCountByCriteria(campus, termCode, createdDateFrom, createdDateTo, status);
    }

    @Override
    public List<EarlyAlertReasonCountsTO> getStudentEarlyAlertReasonCountByCriteria(String termCode,
            Date createdDateFrom, Date createdDateTo, Campus campus, ObjectStatus objectStatus) {
        return dao.getStudentEarlyAlertReasonCountByCriteria(termCode, createdDateFrom, createdDateTo, campus,
                objectStatus);
    }

    @Override
    public PagingWrapper<EntityStudentCountByCoachTO> getStudentEarlyAlertCountByCoaches(
            EntityCountByCoachSearchForm form) {
        return dao.getStudentEarlyAlertCountByCoaches(form);
    }

    @Override
    public Long getEarlyAlertCountSetForCriteria(EarlyAlertStudentSearchTO searchForm) {
        return dao.getEarlyAlertCountSetForCriteria(searchForm);
    }

    @Override
    public void sendAllEarlyAlertReminderNotifications() {
        Date lastResponseDate = getMinimumResponseComplianceDate();
        // if no responseDate is given no emails are sent
        if (lastResponseDate == null) {
            return;
        }
        List<EarlyAlert> eaOutOfCompliance = dao.getResponseDueEarlyAlerts(lastResponseDate);
        LOGGER.debug("Early Alerts out of compliance: {}", eaOutOfCompliance.size());
        Map<UUID, List<EarlyAlertMessageTemplateTO>> easByCoach = new HashMap<UUID, List<EarlyAlertMessageTemplateTO>>();
        Map<UUID, Person> coaches = new HashMap<UUID, Person>();
        final boolean includeCoachAsRecipient = this.earReminderRecipientConfig.includeCoachAsRecipient();
        final boolean includeEarlyAlertCoordinatorAsRecipient = this.earReminderRecipientConfig
                .includeEarlyAlertCoordinatorAsRecipient();
        final boolean includeEarlyAlertCoordinatorAsRecipientOnlyIfStudentHasNoCoach = this.earReminderRecipientConfig
                .includeEarlyAlertCoordinatorAsRecipientOnlyIfStudentHasNoCoach();
        LOGGER.info("Config: includeCoachAsRecipient(): {}", includeCoachAsRecipient);
        LOGGER.info("Config: includeEarlyAlertCoordinatorAsRecipient(): {}",
                includeEarlyAlertCoordinatorAsRecipient);
        LOGGER.info("Config: includeEarlyAlertCoordinatorAsRecipientOnlyIfStudentHasNoCoach(): {}",
                includeEarlyAlertCoordinatorAsRecipientOnlyIfStudentHasNoCoach);
        for (EarlyAlert earlyAlert : eaOutOfCompliance) {
            final Set<Person> recipients = new HashSet<Person>();
            Person coach = earlyAlert.getPerson().getCoach();
            if (includeCoachAsRecipient) {
                if (coach == null) {
                    LOGGER.warn(
                            "Early Alert with id: {} is associated with a person without a coach, so skipping email to coach.",
                            earlyAlert.getId());
                } else {
                    recipients.add(coach);
                }
            }
            if (includeEarlyAlertCoordinatorAsRecipient
                    || (coach == null && includeEarlyAlertCoordinatorAsRecipientOnlyIfStudentHasNoCoach)) {
                final Campus campus = earlyAlert.getCampus();
                if (campus == null) {
                    LOGGER.error("Early Alert with id: {} does not have valid a campus, so skipping email to EAC.",
                            earlyAlert.getId());
                } else {
                    final UUID earlyAlertCoordinatorId = campus.getEarlyAlertCoordinatorId();
                    if (earlyAlertCoordinatorId == null) {
                        LOGGER.error(
                                "Early Alert with id: {} has campus with no early alert coordinator, so skipping email to EAC.",
                                earlyAlert.getId());
                    } else {
                        try {
                            final Person earlyAlertCoordinator = personService.get(earlyAlertCoordinatorId);
                            if (earlyAlertCoordinator == null) { // guard against change in behavior where ObjectNotFoundException is not thrown (which we've seen)
                                LOGGER.error(
                                        "Early Alert with id: {} has campus with an early alert coordinator with a bad ID ({}), so skipping email to EAC.",
                                        earlyAlert.getId(), earlyAlertCoordinatorId);
                            } else {
                                recipients.add(earlyAlertCoordinator);
                            }
                        } catch (ObjectNotFoundException exp) {
                            LOGGER.error(
                                    "Early Alert with id: {} has campus with an early alert coordinator with a bad ID ({}), so skipping email to coach because no coach can be resolved.",
                                    new Object[] { earlyAlert.getId(), earlyAlertCoordinatorId, exp });
                        }
                    }
                }
            }
            LOGGER.debug("Early Alert: {}; Recipients: {}", earlyAlert.getId(), recipients);
            if (recipients.isEmpty()) {
                continue;
            } else {
                for (Person person : recipients) {
                    // We've definitely got a coach by this point
                    if (easByCoach.containsKey(person.getId())) {
                        final List<EarlyAlertMessageTemplateTO> coachEarlyAlerts = easByCoach.get(person.getId());
                        coachEarlyAlerts.add(createEarlyAlertTemplateTO(earlyAlert));
                    } else {
                        coaches.put(person.getId(), person);
                        final ArrayList<EarlyAlertMessageTemplateTO> eam = Lists.newArrayList();
                        eam.add(createEarlyAlertTemplateTO(earlyAlert)); // add separately from newArrayList() call else list will be sized to 1
                        easByCoach.put(person.getId(), eam);
                    }
                }
            }
            List<WatchStudent> watchers = earlyAlert.getPerson().getWatchers();
            for (WatchStudent watcher : watchers) {
                if (easByCoach.containsKey(watcher.getPerson().getId())) {
                    final List<EarlyAlertMessageTemplateTO> coachEarlyAlerts = easByCoach
                            .get(watcher.getPerson().getId());
                    coachEarlyAlerts.add(createEarlyAlertTemplateTO(earlyAlert));
                } else {
                    coaches.put(watcher.getPerson().getId(), watcher.getPerson());
                    final ArrayList<EarlyAlertMessageTemplateTO> eam = Lists.newArrayList();
                    eam.add(createEarlyAlertTemplateTO(earlyAlert)); // add separately from newArrayList() call else list will be sized to 1
                    easByCoach.put(watcher.getPerson().getId(), eam);
                }
            }
        }
        for (UUID coachId : easByCoach.keySet()) {
            Map<String, Object> messageParams = new HashMap<String, Object>();

            Collections.sort(easByCoach.get(coachId), new Comparator<EarlyAlertTO>() {
                @Override
                public int compare(EarlyAlertTO p1, EarlyAlertTO p2) {
                    Date p1Date = p1.getLastResponseDate();
                    if (p1Date == null)
                        p1Date = p1.getCreatedDate();
                    Date p2Date = p2.getLastResponseDate();
                    if (p2Date == null)
                        p2Date = p2.getCreatedDate();
                    return p1Date.compareTo(p2Date);
                }

            });

            Integer daysSince1900ResponseExpected = DateTimeUtils.daysSince1900(lastResponseDate);
            List<Pair<EarlyAlertMessageTemplateTO, Integer>> earlyAlertTOPairs = new ArrayList<Pair<EarlyAlertMessageTemplateTO, Integer>>();
            for (EarlyAlertMessageTemplateTO ea : easByCoach.get(coachId)) {
                Integer daysOutOfCompliance;
                if (ea.getLastResponseDate() != null) {
                    daysOutOfCompliance = daysSince1900ResponseExpected
                            - DateTimeUtils.daysSince1900(ea.getLastResponseDate());
                } else {
                    daysOutOfCompliance = daysSince1900ResponseExpected
                            - DateTimeUtils.daysSince1900(ea.getCreatedDate());
                }

                // Just in case attempt to only send emails for EA full day out of compliance
                if (daysOutOfCompliance >= 0)
                    earlyAlertTOPairs.add(new Pair<EarlyAlertMessageTemplateTO, Integer>(ea, daysOutOfCompliance));
            }
            messageParams.put("earlyAlertTOPairs", earlyAlertTOPairs);
            messageParams.put("coach", coaches.get(coachId));
            messageParams.put("DateTimeUtils", DateTimeUtils.class);
            messageParams.put("termToRepresentEarlyAlert",
                    configService.getByNameEmpty("term_to_represent_early_alert"));

            SubjectAndBody subjAndBody = messageTemplateService
                    .createEarlyAlertResponseRequiredToCoachMessage(messageParams);
            try {
                messageService.createMessage(coaches.get(coachId), null, subjAndBody);
            } catch (Exception exp) {
                LOGGER.error(
                        "Unable to send reminder emails to coach: " + coaches.get(coachId).getFullName() + "\n",
                        exp);
            }
        }

    }

    private EarlyAlertMessageTemplateTO createEarlyAlertTemplateTO(EarlyAlert earlyAlert) {
        Person creator = null;
        try {
            creator = personService.get(earlyAlert.getCreatedBy().getId());
        } catch (ObjectNotFoundException exp) {
            LOGGER.error("Early Alert with id: " + earlyAlert.getId() + " does not have valid creator: "
                    + earlyAlert.getCreatedBy(), exp);
        }
        return new EarlyAlertMessageTemplateTO(earlyAlert, creator,
                earlyAlert.getPerson().getWatcherEmailAddresses());
    }

    public Map<UUID, Number> getResponsesDueCountEarlyAlerts(List<UUID> personIds) {
        Date lastResponseDate = getMinimumResponseComplianceDate();
        if (lastResponseDate == null)
            return new HashMap<UUID, Number>();
        return dao.getResponsesDueCountEarlyAlerts(personIds, lastResponseDate);
    }

    private Date getMinimumResponseComplianceDate() {
        final String numVal = configService.getByNameNull("maximum_days_before_early_alert_response");
        if (StringUtils.isBlank(numVal))
            return null;
        Integer allowedDaysPastResponse = Integer.parseInt(numVal);

        return DateTimeUtils.getDateOffsetInDays(new Date(), -allowedDaysPastResponse);

    }

    @Override
    public PagedResponse<EarlyAlertSearchResultTO> searchEarlyAlert(EarlyAlertSearchForm form) {
        PagingWrapper<EarlyAlertSearchResult> models = dao.searchEarlyAlert(form);
        return new PagedResponse<EarlyAlertSearchResultTO>(true, models.getResults(),
                searchResultFactory.asTOList(models.getRows()));
    }
}