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

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.MapStatusReportCalcTaskImpl.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 java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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 java.util.concurrent.Callable;

import com.google.common.collect.Lists;

import org.apache.commons.lang.StringUtils;
import org.jasig.ssp.dao.external.ExternalPersonDao;
import org.jasig.ssp.model.MapStatusReport;
import org.jasig.ssp.model.Person;
import org.jasig.ssp.model.SubjectAndBody;
import org.jasig.ssp.model.WatchStudent;
import org.jasig.ssp.model.external.ExternalStudentTranscriptCourse;
import org.jasig.ssp.model.external.ExternalSubstitutableCourse;
import org.jasig.ssp.model.external.Term;
import org.jasig.ssp.service.MapStatusReportService;
import org.jasig.ssp.service.MessageService;
import org.jasig.ssp.service.ObjectNotFoundException;
import org.jasig.ssp.service.PlanService;
import org.jasig.ssp.service.external.ExternalStudentTranscriptCourseService;
import org.jasig.ssp.service.external.MapStatusReportCalcTask;
import org.jasig.ssp.service.external.TermService;
import org.jasig.ssp.service.reference.ConfigService;
import org.jasig.ssp.service.reference.MessageTemplateService;
import org.jasig.ssp.transferobject.reports.MapStatusReportOwnerAndCoachInfo;
import org.jasig.ssp.transferobject.reports.MapStatusReportPerson;
import org.jasig.ssp.transferobject.reports.MapStatusReportSummary;
import org.jasig.ssp.transferobject.reports.MapStatusReportSummaryDetail;
import org.jasig.ssp.util.CallableExecutor;
import org.jasig.ssp.util.transaction.WithTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MapStatusReportCalcTaskImpl implements MapStatusReportCalcTask {

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

    @Autowired
    private transient PlanService planService;

    @Autowired
    private transient TermService termService;

    @Autowired
    private transient ConfigService configService;

    @Autowired
    private transient MapStatusReportService mapStatusReportService;

    @Autowired
    private transient ExternalStudentTranscriptCourseService externalStudentTranscriptCourseService;

    @Autowired
    private transient ExternalPersonDao dao;

    @Autowired
    private WithTransaction withTransaction;

    @Autowired
    private MessageService messageService;

    @Autowired
    protected transient MessageTemplateService messageTemplateService;

    public Class<Void> getBatchExecReturnType() {
        return Void.TYPE;
    }

    // intentionally not transactional... this is the main loop, each iteration
    // of which should be its own transaction.
    @Override
    public void exec(CallableExecutor<Void> batchExecutor) {

        if (Thread.currentThread().isInterrupted()) {
            LOGGER.info("Abandoning map status report calculation because of thread interruption");
            return;
        }
        if (!Boolean.parseBoolean(configService.getByNameEmpty("calculate_map_plan_status").trim())) {
            LOGGER.info(
                    "Map Plan Status Report calculation will not execute because the property calculate_map_plan_status is set to false");
            return;
        }
        LOGGER.info("BEGIN : MAPSTATUS REPORT ");

        MapStatusReportSummary summary = new MapStatusReportSummary();
        summary.setStartTime(Calendar.getInstance());

        //Hard delete all previous reports
        mapStatusReportService.deleteAllOldReports();

        final boolean useSubstitutableCourses = Boolean
                .parseBoolean(configService.getByNameEmpty("map_plan_status_use_substitutable_courses").trim());
        final Collection<ExternalSubstitutableCourse> allSubstitutableCourses = useSubstitutableCourses
                ? mapStatusReportService.getAllSubstitutableCourses()
                : Lists.<ExternalSubstitutableCourse>newArrayList();

        //Load up our configs
        final Set<String> gradesSet = mapStatusReportService.getPassingGrades();
        final Set<String> additionalCriteriaSet = mapStatusReportService.getAdditionalCriteria();
        final boolean termBound = Boolean
                .parseBoolean(configService.getByNameEmpty("map_plan_status_term_bound_strict").trim());

        //Lets figure out our cutoff term
        final Term cutoffTerm = mapStatusReportService.deriveCuttoffTerm();

        //Lightweight query to avoid the potential 'kitchen sink' we would pull out if we fetched the Plan object
        List<MapStatusReportPerson> allActivePlans = planService.getAllActivePlanIds();
        LOGGER.info("Starting report calculations for {} plans", allActivePlans.size());

        final List<Term> allTerms = termService.getAll();
        //Sort terms by startDate, we do this here so we have no dependency on the default sort order in termService.getAll()
        sortTerms(allTerms);

        //Iterate through the active plans.  A transaction is committed after each plan
        for (final MapStatusReportPerson planIdPersonIdPair : allActivePlans) {

            if (Thread.currentThread().isInterrupted()) {
                LOGGER.info("Abandoning map status report calculation because of thread interruption");
                return;
            }

            LOGGER.info("MAP STATUS REPORT CALCULATION STARTING FOR: " + planIdPersonIdPair.getSchoolId());

            if (batchExecutor == null) {
                evaluatePlan(gradesSet, additionalCriteriaSet, cutoffTerm, allTerms, planIdPersonIdPair,
                        allSubstitutableCourses, termBound, useSubstitutableCourses);
            } else {
                try {
                    batchExecutor.exec(new Callable<Void>() {
                        @Override
                        public Void call() throws Exception {
                            evaluatePlan(gradesSet, additionalCriteriaSet, cutoffTerm, allTerms, planIdPersonIdPair,
                                    allSubstitutableCourses, termBound, useSubstitutableCourses);
                            return null;
                        }
                    });
                } catch (RuntimeException e) {
                    throw e;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

            LOGGER.info("FINISHED MAP STATUS REPORT CALCULATION FOR: " + planIdPersonIdPair.getSchoolId());
        }
        summary.setEndTime(Calendar.getInstance());
        summary.setStudentsInScope(allActivePlans.size());

        sendReportEmail(summary);
        sendOffPlanEmailsToCoaches();

        LOGGER.info("MAPSTATUS REPORT RUNTIME: "
                + (summary.getEndTime().getTimeInMillis() - summary.getStartTime().getTimeInMillis()) + " ms.");
        LOGGER.info("END : MAPSTATUS REPORT ");

    }

    private void sendOffPlanEmailsToCoaches() {
        boolean sendEmail = Boolean.parseBoolean(
                configService.getByNameEmpty("map_plan_status_send_off_plan_coach_email").trim().toLowerCase());
        if (!(sendEmail)) {
            return;
        }

        final List<MapStatusReportOwnerAndCoachInfo> distinctOwnerCoachPairs = mapStatusReportService
                .getOwnersAndCoachesWithOffPlanStudent();
        final List<MapStatusReportOwnerAndCoachInfo> distinctWatchers = mapStatusReportService
                .getWatchersOffPlanStudent();

        if (distinctOwnerCoachPairs == null || distinctOwnerCoachPairs.isEmpty()) {
            return;
        }

        final Set<UUID> ownerIds = new HashSet<UUID>();
        final Map<UUID, String> emailAddressByPersonId = new HashMap<UUID, String>();
        final Map<UUID, Map<PersonToPlanRelationship, List<MapStatusReportPerson>>> statusesByOwnerOrCoachOrWatcher = new HashMap<UUID, Map<PersonToPlanRelationship, List<MapStatusReportPerson>>>();

        for (MapStatusReportOwnerAndCoachInfo ownerCoachPair : distinctOwnerCoachPairs) {
            final UUID ownerId = ownerCoachPair.getOwnerId();
            final UUID coachId = ownerCoachPair.getCoachId();

            emailAddressByPersonId.put(ownerId, ownerCoachPair.getOwnerPrimaryEmail());

            if (coachId != null) {
                emailAddressByPersonId.put(coachId, ownerCoachPair.getCoachPrimaryEmail());
            }

            ownerIds.add(ownerId);

            if (!(statusesByOwnerOrCoachOrWatcher.containsKey(ownerId))) {
                statusesByOwnerOrCoachOrWatcher.put(ownerId, newPlanStatusContainerForOffPlanEmailsToCoaches());
            }
            if (coachId != null && !(statusesByOwnerOrCoachOrWatcher.containsKey(coachId))) {
                statusesByOwnerOrCoachOrWatcher.put(coachId, newPlanStatusContainerForOffPlanEmailsToCoaches());
            }
        }
        for (MapStatusReportOwnerAndCoachInfo mapStatusReportOwnerAndCoachInfo : distinctWatchers) {
            final UUID watcherId = mapStatusReportOwnerAndCoachInfo.getWatcherId();
            ownerIds.add(watcherId);
            if (!(statusesByOwnerOrCoachOrWatcher.containsKey(watcherId))) {
                statusesByOwnerOrCoachOrWatcher.put(watcherId, newPlanStatusContainerForOffPlanEmailsToCoaches());
            }

        }
        for (UUID ownerId : ownerIds) {
            final List<MapStatusReportPerson> offPlanPlansForOwner = mapStatusReportService
                    .getOffPlanPlansForOwner(new Person(ownerId));
            //         if ( offPlanPlansForOwner == null || offPlanPlansForOwner.isEmpty() ) {
            //            continue;
            //         }
            for (MapStatusReportPerson status : offPlanPlansForOwner) {
                final UUID planCoachId = status.getCoachId();
                final boolean sameOwnerAndCoach = ownerId.equals(planCoachId);
                final Map<PersonToPlanRelationship, List<MapStatusReportPerson>> statusesForOwner = statusesByOwnerOrCoachOrWatcher
                        .get(ownerId);
                if (sameOwnerAndCoach) {
                    final List<MapStatusReportPerson> categorizedStatuses = statusesForOwner
                            .get(PersonToPlanRelationship.OWNER_AND_COACH);
                    categorizedStatuses.add(status);
                } else {
                    final List<MapStatusReportPerson> ownerCategorizedStatuses = statusesForOwner
                            .get(PersonToPlanRelationship.OWNER_ONLY);
                    ownerCategorizedStatuses.add(status);

                    if (planCoachId != null) {
                        final Map<PersonToPlanRelationship, List<MapStatusReportPerson>> statusesForCoach = statusesByOwnerOrCoachOrWatcher
                                .get(planCoachId);
                        // Seen this lookup come up empty in practice. Possibly b/c coach assignments changed since
                        // the original list of owners and coaches was pulled? Not really worth it to try to
                        // read/repair here, e.g. would have to go look up the coach email. So for now we're just
                        // going to skip such coaches.
                        if (statusesForCoach != null) {
                            final List<MapStatusReportPerson> coachCategorizedStatuses = statusesForCoach
                                    .get(PersonToPlanRelationship.COACH_ONLY);
                            coachCategorizedStatuses.add(status);
                        } else {
                            LOGGER.info(
                                    "Skipping status notification for coach {} on plan {} because that coach wasn't in the original list of plan owners and coaches",
                                    planCoachId, status.getPlanId());
                        }
                    }
                }
            }
            final List<MapStatusReportPerson> offPlanPlansForWatcher = mapStatusReportService
                    .getOffPlanPlansForWatcher(new Person(ownerId));
            for (MapStatusReportPerson mapStatusReportPerson : offPlanPlansForWatcher) {
                //If watcher is was already owner/coach of a student, it has already been added
                final boolean isOwnerOrCoach = ownerId.equals(mapStatusReportPerson.getOwnerId())
                        || ownerId.equals(mapStatusReportPerson.getCoachId());
                if (!isOwnerOrCoach) {
                    final Map<PersonToPlanRelationship, List<MapStatusReportPerson>> statusesForPerson = statusesByOwnerOrCoachOrWatcher
                            .get(ownerId);
                    List<MapStatusReportPerson> watcherPlans = statusesForPerson
                            .get(PersonToPlanRelationship.WATCHER_ONLY);
                    watcherPlans.add(mapStatusReportPerson);

                }

            }

        }

        final MapStatusReportPersonNameComparator sorter = new MapStatusReportPersonNameComparator();
        for (Map.Entry<UUID, Map<PersonToPlanRelationship, List<MapStatusReportPerson>>> statusesForOwnerOrCoach : statusesByOwnerOrCoachOrWatcher
                .entrySet()) {

            final UUID sendToPersonId = statusesForOwnerOrCoach.getKey();
            final String sendToEmailAddress = emailAddressByPersonId.get(sendToPersonId);
            if (StringUtils.trimToNull(sendToEmailAddress) == null) {
                continue;
            }

            for (List<MapStatusReportPerson> statuses : statusesForOwnerOrCoach.getValue().values()) {
                Collections.sort(statuses, sorter);
            }

            // TODO Templatize: https://issues.jasig.org/browse/SSP-2572
            final Map<PersonToPlanRelationship, List<MapStatusReportPerson>> statusesByCategory = statusesForOwnerOrCoach
                    .getValue();
            final StringBuilder sb = new StringBuilder();
            sb.append("<html><body>\n").append("<h2>MAP Plan Status Report</h2>\n").append(
                    "The following students have been determined to be Off Plan after comparing their transcript to their MAP.</br>\n");

            appendOffPlanStanza(statusesByCategory, PersonToPlanRelationship.OWNER_AND_COACH,
                    "Assigned to You (and You Planned the MAP)", sb);
            appendOffPlanStanza(statusesByCategory, PersonToPlanRelationship.COACH_ONLY,
                    "Assigned to You (but Somebody Else Planned the MAP)", sb);
            appendOffPlanStanza(statusesByCategory, PersonToPlanRelationship.OWNER_ONLY,
                    "Not Assigned to You (but You Planned the MAP)", sb);
            appendOffPlanStanza(statusesByCategory, PersonToPlanRelationship.WATCHER_ONLY,
                    "Plans of Students you watch (but you did not Plan the map or are not the coach) ", sb);

            sb.append("<br/>\n</body></html>");

            SubjectAndBody subjectAndBody = new SubjectAndBody("Student MAP Off Plan Report", sb.toString());
            try {
                messageService.createMessage(sendToEmailAddress, null, subjectAndBody);
            } catch (ObjectNotFoundException e) {
                LOGGER.error("Failed to send MAP status report to owner or coach {} at address {}",
                        new Object[] { sendToPersonId, sendToEmailAddress, e });
            }

        }
    }

    private static enum PersonToPlanRelationship {
        OWNER_ONLY, COACH_ONLY, OWNER_AND_COACH, WATCHER_ONLY
    }

    private void appendOffPlanStanza(Map<PersonToPlanRelationship, List<MapStatusReportPerson>> statusesByCategory,
            PersonToPlanRelationship statusCategory, String categoryStanzaHeader, StringBuilder appendTo) {
        final List<MapStatusReportPerson> statusesInCategory = statusesByCategory.get(statusCategory);
        appendTo.append("<h3>").append(categoryStanzaHeader).append("</h3>\n");
        if (statusesInCategory.isEmpty()) {
            appendTo.append("<em>None</em>\n");
            appendTo.append("<br/>\n");
        } else {
            for (MapStatusReportPerson status : statusesInCategory) {
                appendTo.append(status.getFirstName()).append(" ").append(status.getLastName()).append("<br/>\n");
            }
        }
    }

    private Map<PersonToPlanRelationship, List<MapStatusReportPerson>> newPlanStatusContainerForOffPlanEmailsToCoaches() {
        final Map<PersonToPlanRelationship, List<MapStatusReportPerson>> plansContainer = new HashMap<PersonToPlanRelationship, List<MapStatusReportPerson>>();

        for (PersonToPlanRelationship key : PersonToPlanRelationship.values()) {
            plansContainer.put(key, Lists.<MapStatusReportPerson>newArrayList());
        }

        return plansContainer;
    }

    private static final class MapStatusReportPersonNameComparator implements Comparator<MapStatusReportPerson> {
        @Override
        public int compare(MapStatusReportPerson msrp1, MapStatusReportPerson msrp2) {
            return nameOf(msrp1).compareTo(nameOf(msrp2));
        }

        private String nameOf(MapStatusReportPerson msrp) {
            return new StringBuilder().append(StringUtils.trimToEmpty(msrp.getLastName()))
                    .append(StringUtils.trimToEmpty(msrp.getFirstName())).toString();
        }
    }

    private void sendReportEmail(MapStatusReportSummary summary) {
        boolean sendEmail = Boolean.parseBoolean(
                configService.getByNameEmpty("map_plan_status_send_report_email").trim().toLowerCase());
        if (sendEmail) {
            List<MapStatusReportSummaryDetail> details = mapStatusReportService.getSummaryDetails();
            for (MapStatusReportSummaryDetail mapStatusReportSummaryDetail : details) {
                LOGGER.info("MAPSTATUSREPORT SUMMARY: " + mapStatusReportSummaryDetail.getPlanStatus() + " COUNT: "
                        + mapStatusReportSummaryDetail.getCount());
            }
            summary.setSummaryDetails(details);
            SubjectAndBody mapStatusEmail = messageTemplateService.createMapStatusReportEmail(summary);
            String mapEmail = configService.getByNameEmpty("map_plan_status_email").trim();
            if (!StringUtils.isEmpty(mapEmail)) {
                try {
                    messageService.createMessage(mapEmail, null, mapStatusEmail);
                } catch (ObjectNotFoundException e) {
                    LOGGER.error("There was an error sending the map status report email", e);
                }
            }
        }
    }

    private void evaluatePlan(Set<String> gradesSet, Set<String> criteriaSet, Term cutoffTerm, List<Term> allTerms,
            MapStatusReportPerson planIdPersonIdPair,
            Collection<ExternalSubstitutableCourse> allSubstitutableCourses, boolean termBound,
            boolean useSubstitutableCourses) {
        List<ExternalStudentTranscriptCourse> transcript = externalStudentTranscriptCourseService
                .getTranscriptsBySchoolId(planIdPersonIdPair.getSchoolId());
        final MapStatusReport report = mapStatusReportService.evaluatePlan(gradesSet, criteriaSet, cutoffTerm,
                allTerms, planIdPersonIdPair, allSubstitutableCourses, transcript, termBound,
                useSubstitutableCourses);
        try {
            //Any new writes to this task should be included here
            withTransaction.withNewTransaction(new Callable<MapStatusReport>() {

                @Override
                public MapStatusReport call() throws Exception {
                    return mapStatusReportService.save(report);
                }
            });
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
        }
    }

    private void sortTerms(List<Term> allTerms) {
        Collections.sort(allTerms, new Comparator<Term>() {

            @Override
            public int compare(Term o1, Term o2) {
                if (o1.getStartDate().before(o2.getStartDate()))
                    return -1;
                if (o1.getStartDate().after(o2.getStartDate()))
                    return 1;
                //Hopefully this isnt ever the case
                if (o1.getStartDate().equals(o2.getStartDate()))
                    return 0;
                return 0;
            }
        });
    }
}