org.opentestsystem.delivery.testadmin.scheduling.Scheduler.java Source code

Java tutorial

Introduction

Here is the source code for org.opentestsystem.delivery.testadmin.scheduling.Scheduler.java

Source

/*
Educational Online Test Delivery System Copyright (c) 2013 American Institutes for Research
    
Distributed under the AIR Open Source License, Version 1.0 See accompanying file AIR-License-1_0.txt or at
http://www.smarterapp.org/documents/American_Institutes_for_Research_Open_Source_Software_License.pdf
 */

package org.opentestsystem.delivery.testadmin.scheduling;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;

import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.opentestsystem.delivery.testadmin.domain.Facility;
import org.opentestsystem.delivery.testadmin.domain.Facility.SeatConfiguration;
import org.opentestsystem.delivery.testadmin.domain.FacilityAvailability;
import org.opentestsystem.delivery.testadmin.domain.FacilityAvailability.FacilityTimeSlot;
import org.opentestsystem.delivery.testadmin.domain.Seat;
import org.opentestsystem.delivery.testadmin.domain.schedule.Schedule;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduleCreationInfo;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledDay;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledFacility;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledSeat;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledStudent;
import org.opentestsystem.delivery.testadmin.domain.schedule.ScheduledTimeSlot;
import org.opentestsystem.delivery.testreg.domain.Assessment;
import org.opentestsystem.delivery.testreg.domain.ARTHelpers;
import org.opentestsystem.delivery.testreg.domain.EligibleStudent;
import org.opentestsystem.delivery.testreg.domain.HierarchyLevel;
import org.opentestsystem.delivery.testreg.domain.InstitutionEntity;
import org.opentestsystem.delivery.testreg.domain.Sb11Entity;
import org.opentestsystem.delivery.testreg.domain.StudentGroup;
import org.opentestsystem.delivery.testreg.service.Sb11EntityRepositoryService;
import org.opentestsystem.delivery.testreg.service.TestRegPersister;
import org.opentestsystem.shared.progman.client.domain.Tenant;
import org.opentestsystem.shared.progman.client.domain.TenantType;
import org.opentestsystem.shared.security.domain.SbacEntity;
import org.opentestsystem.shared.security.service.TenancyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * Main entry into scheduling. Most access will be performed through the createSchedule() method.
 * 
 */
@Component
public class Scheduler {

    @Autowired
    private SchedulerHelper schedulerHelper;

    @Autowired
    private ScheduleTestStatusCreator scheduleTestStatusCreator;

    @Autowired
    private Sb11EntityRepositoryService sb11EntityService;

    @Autowired
    private TenancyService tenancyService;

    @Autowired
    private TestRegPersister testRegPersister;

    /**
     * Creates a schedule using a partially filled in schedule object as input
     * 
     * @param inSchedule
     * @return
     */
    public Schedule createSchedule(final Schedule inSchedule) {

        boolean fakeScheduleIdUsed = false;
        String scheduleId = inSchedule.getId();
        if (inSchedule.getId() == null) {
            // generate a uuid to use as a unique identifier until the schedule is saved so that we can use it to identify
            // test statuses from
            // this particular schedule
            scheduleId = UUID.randomUUID().toString();
            fakeScheduleIdUsed = true;
        }

        // use the schedule helper to gather up data we need
        Map<String, FacilityData> facilityData = schedulerHelper.findFacilityData(inSchedule.getInstitutionId());

        List<EligibleStudent> eligibleStudents = schedulerHelper.findStudents(inSchedule.getInstitutionId());

        List<String> tenantIdsForAssessment = getTenantIds(inSchedule.getInstitutionId());

        List<Assessment> assessments = schedulerHelper.findAssessments(eligibleStudents, inSchedule.getStartDate(),
                inSchedule.getEndDate(), tenantIdsForAssessment);

        eligibleStudents = schedulerHelper.filterStudents(eligibleStudents, assessments);

        // generate the entire structure needed to perform all the allocations
        Schedule scheduled = generateScheduleStructure(inSchedule, facilityData);

        scheduled.setId(scheduleId);

        // create a simple map to capture whether or not each of the students have been scheduled
        Map<String, ScheduledStudent> studentsScheduled = createStudentsScheduledMap(eligibleStudents, assessments);

        // create a multimap that associates a grade to assessments that students of that grade are eligible for
        HashMultimap<String, Assessment> gradesToAssessments = createGradesToAssessmentsMap(studentsScheduled);

        // create a multimap that associates a student groups to assessments via student
        HashMultimap<String, Assessment> studentGroupToAssessments = createStudentGroupToAssessmentMap(
                studentsScheduled, scheduled.getInstitutionId());

        // put all the time slots in order to prepare for scheduling
        scheduled.generateOrderedTimeSlots();

        schedulerHelper.determineStudentAccessEquip(studentsScheduled, scheduled, false);

        // iterate through time slots and allocate for any that have affinities
        schedulerHelper.allocateTimeslotAffinities(scheduled, assessments, studentsScheduled, gradesToAssessments,
                false);

        // are there still students that need to be scheduled?
        if (countStudentsToSchedule(studentsScheduled) > 0) {
            // if there are priority allocation rules defined for this schedule, allocate by priority
            if (scheduled.hasPriorityAllocationRules()) {
                schedulerHelper.allocateWithPriorities(scheduled, assessments, studentsScheduled,
                        gradesToAssessments, studentGroupToAssessments, false);

                if (countStudentsToSchedule(studentsScheduled) > 0) {
                    schedulerHelper.allocateAll(scheduled, assessments, studentsScheduled, gradesToAssessments,
                            false);
                }

            } else {
                // if no priority allocation rules, then run generic allocation process
                schedulerHelper.allocateAll(scheduled, assessments, studentsScheduled, gradesToAssessments, false);
            }
        }

        // next is proctor allocation
        schedulerHelper.allocateProctors(scheduled, gradesToAssessments, false);

        // now do some validation to ensure that everything is scheduled
        ScheduleCreationInfo info = schedulerHelper.validateSchedule(scheduled, eligibleStudents, studentsScheduled,
                false);

        scheduled.setCreationInfo(info);
        scheduleTestStatusCreator.writeForSchedule(scheduleId);
        if (fakeScheduleIdUsed) {
            // remove the fake id so that mongo generates its own id when the Schedule is saved
            scheduled.setId(null);
        }
        return scheduled;
    }

    /**
     * Pass in a filled in or partially filled in schedule and this function will figure out which students still need
     * to be scheduled and attempt to fit them into the empty spots of the schedule.
     * 
     * @param inSchedule
     * @return
     */
    public Schedule reschedule(final Schedule inSchedule) {

        List<EligibleStudent> eligibleStudents = schedulerHelper.findStudents(inSchedule.getInstitutionId());

        List<String> tenantIdsForAssessment = getTenantIds(inSchedule.getInstitutionId());

        List<Assessment> assessments = schedulerHelper.findAssessments(eligibleStudents, inSchedule.getStartDate(),
                inSchedule.getEndDate(), tenantIdsForAssessment);

        eligibleStudents = schedulerHelper.filterStudents(eligibleStudents, assessments);

        // create a simple map to capture whether or not each of the students have been scheduled
        Map<String, ScheduledStudent> studentsScheduled = createStudentsScheduledMap(eligibleStudents, assessments);

        // put all the time slots in order to prepare for scheduling
        inSchedule.generateOrderedTimeSlots();

        // go through studentsScheduled to figure out which students are already scheduled and which aren't
        // filter studentsScheduled so it only has the students who need to be scheduled

        Map<String, ScheduledStudent> studentsScheduledFiltered = filterOnlyUnscheduledStudents(studentsScheduled,
                inSchedule.getOrderedTimeSlots(false));

        // create a multimap that associates a grade to assessments that students of that grade are eligible for
        HashMultimap<String, Assessment> gradesToAssessments = createGradesToAssessmentsMap(
                studentsScheduledFiltered);

        // create a multimap that associates a student groups to assessments via student
        HashMultimap<String, Assessment> studentGroupToAssessments = createStudentGroupToAssessmentMap(
                studentsScheduled, inSchedule.getInstitutionId());

        // run the rest of scheduling here using studentsScheduledFiltered

        // iterate through time slots and allocate for any that have affinities
        schedulerHelper.allocateTimeslotAffinities(inSchedule, assessments, studentsScheduledFiltered,
                gradesToAssessments, true);

        // are there still students that need to be scheduled?
        if (countStudentsToSchedule(studentsScheduledFiltered) > 0) {
            // if there are priority allocation rules defined for this schedule, allocate by priority
            if (inSchedule.hasPriorityAllocationRules()) {
                schedulerHelper.allocateWithPriorities(inSchedule, assessments, studentsScheduledFiltered,
                        studentGroupToAssessments, gradesToAssessments, true);

                if (countStudentsToSchedule(studentsScheduled) > 0) {
                    schedulerHelper.allocateAll(inSchedule, assessments, studentsScheduledFiltered,
                            gradesToAssessments, true);
                }

            } else {
                // if no priority allocation rules, then run generic allocation process
                schedulerHelper.allocateAll(inSchedule, assessments, studentsScheduledFiltered, gradesToAssessments,
                        true);
            }
        }

        // next is proctor allocation
        schedulerHelper.allocateProctors(inSchedule, gradesToAssessments, true);

        // now do some validation to ensure that everything is scheduled
        ScheduleCreationInfo info = schedulerHelper.validateSchedule(inSchedule, eligibleStudents,
                studentsScheduledFiltered, true);

        inSchedule.setCreationInfo(info);

        scheduleTestStatusCreator.writeForSchedule(inSchedule.getId());

        return inSchedule;
    }

    private Schedule generateScheduleStructure(final Schedule inSchedule,
            final Map<String, FacilityData> facilityData) {
        // build out the full Schedule structure
        // Create a List of ScheduledDay objects, one object for each day in the schedule
        // Each ScheduledDay object contains a List of ScheduledFacility objects, one for each Facility for this
        // institution
        // Each ScheduledFacility contains a list of ScheduledTimeSlots which are based upon the FacilityTimeSlot List
        // in each FacilityAvailability object
        // Each ScheduledTimeSlot contains a List of ScheduledSeat objects which are based off of the SeatConfigurations
        // setup in each FacilityTimeSlot

        Schedule scheduled = null;

        try {
            scheduled = (Schedule) inSchedule.clone();
        } catch (CloneNotSupportedException e) {
            throw new ScheduleException("Failed to clone schedule object, cannot generate schedule ", e);
        }

        TreeMap<DateTime, ScheduledDay> scheduledDaysMap = new TreeMap<DateTime, ScheduledDay>();

        DateTime scheduleStart = scheduled.getStartDate();
        DateTime scheduleEnd = scheduled.getEndDate();

        DateTime curTime = scheduleStart;

        // build all the ScheduledDay objects for this schedule
        while (curTime.isBefore(scheduleEnd) || curTime.equals(scheduleEnd)) {

            if (scheduled.isDoNotScheduleWeekends()) {
                if (curTime.getDayOfWeek() <= DateTimeConstants.FRIDAY) {
                    ScheduledDay sday = new ScheduledDay();
                    sday.setDay(curTime);

                    scheduledDaysMap.put(curTime, sday);
                }
            } else {
                ScheduledDay sday = new ScheduledDay();
                sday.setDay(curTime);

                scheduledDaysMap.put(curTime, sday);
            }

            curTime = curTime.plusDays(1);
        }

        // go ahead and set the ScheduledDay objects into the schedule
        scheduled.setScheduledDays(new ArrayList<ScheduledDay>(scheduledDaysMap.values()));

        // iterate through all the FacilityData objects we have
        // each FacilityData has a Facility and a List of FacilityAvailabilities
        // Need to create a ScheduledFacility for each Facility for each ScheduledDay,
        // The ScheduledFacility has a list of ScheduledTimeSlots that need to be generated based upon
        // the FacilityAvailability objects which state from and to time. Basically, we need to match the date of the
        // ScheduledDay to the from->to range to ensure that the date of the current ScheduledDay falls in there.
        for (Map.Entry<String, FacilityData> entry : facilityData.entrySet()) {
            FacilityData facilData = entry.getValue();

            List<FacilityAvailability> availabilities = facilData.getAvailabilities();
            Facility facility = facilData.getFacility();

            // ensure that the seats are generated from the configurations
            facility.createSeatsFromConfiguration();

            for (FacilityAvailability avail : availabilities) {
                DateTime from = avail.getFromDate();
                DateTime to = avail.getToDate();

                List<FacilityTimeSlot> facilityTimeSlots = avail.getFacilityTimes();

                // iterate through ScheduledDays
                // if the day is in the range for the facility availability, then create a ScheduledFacility object
                // for that ScheduledDay

                for (DateTime schedDate : scheduledDaysMap.keySet()) {
                    if ((from.isEqual(schedDate) || from.isBefore(schedDate))
                            && (to.isEqual(schedDate) || to.isAfter(schedDate))) {
                        ScheduledFacility schedFacil = new ScheduledFacility(facility);

                        // generate scheduled time slots for scheduledFacility

                        for (FacilityTimeSlot slot : facilityTimeSlots) {

                            slot.createSeatsFromConfiguration();

                            ScheduledTimeSlot schedSlot = new ScheduledTimeSlot(slot.getTimeSlot());

                            // create scheduled seats in slot
                            for (SeatConfiguration seatConfig : slot.getSeatConfigurations()) {
                                for (Seat seat : seatConfig.getSeats()) {
                                    schedSlot.addSeat(new ScheduledSeat(seat));
                                }
                            }

                            // modify the date on the ScheduledTimeSlot to have the correct date (from schedDate) to go
                            // along with the specified time (from schedSlot)
                            // This will allow for correct ordering of time slots

                            DateTime newStartDate = new DateTime(schedDate.getYear(), schedDate.getMonthOfYear(),
                                    schedDate.getDayOfMonth(), schedSlot.getStartTime().getHourOfDay(),
                                    schedSlot.getStartTime().getMinuteOfHour());
                            DateTime newEndDate = new DateTime(schedDate.getYear(), schedDate.getMonthOfYear(),
                                    schedDate.getDayOfMonth(), schedSlot.getEndTime().getHourOfDay(),
                                    schedSlot.getEndTime().getMinuteOfHour());

                            schedSlot.setStartTime(newStartDate);
                            schedSlot.setEndTime(newEndDate);

                            schedFacil.addTimeSlot(schedSlot);
                        }

                        scheduledDaysMap.get(schedDate).addFacility(schedFacil);
                    }
                }

            }

        }

        scheduled.generateOrderedTimeSlots();

        return scheduled;
    }

    private Map<String, ScheduledStudent> createStudentsScheduledMap(final List<EligibleStudent> eligibleStudents,
            final List<Assessment> validAssessments) {

        Set<Assessment> assessmentSet = new HashSet<Assessment>(validAssessments);

        int mapSize = (int) (eligibleStudents.size() * 1.3);

        Map<String, ScheduledStudent> studentsScheduledMap = new HashMap<String, ScheduledStudent>(mapSize);

        for (EligibleStudent eligStudent : eligibleStudents) {
            studentsScheduledMap.put(eligStudent.getStudent().getEntityId(),
                    new ScheduledStudent(eligStudent, assessmentSet));
        }

        // get a list of assessments that have statuses for the 1st opportunity in the TestStatus collection for each
        // Student
        // if any of these assessments are in the ScheduledStudent object for each student, remove the entry. The
        // student has already
        // been scheduled or has already taken the Assessment and should not be scheduled again.
        HashMultimap<String, String> scheduledAssessmentsByStudent = scheduleTestStatusCreator
                .findStatusesByStudents(studentsScheduledMap.keySet());

        for (Map.Entry<String, ScheduledStudent> entry : studentsScheduledMap.entrySet()) {
            if (scheduledAssessmentsByStudent.containsKey(entry.getKey())) {
                Set<String> assessmentIdsForStudent = scheduledAssessmentsByStudent.get(entry.getKey());
                Map<Assessment, Boolean> clonedMap = new HashMap<Assessment, Boolean>(
                        entry.getValue().getScheduledAssessments());
                for (Assessment assess : entry.getValue().getScheduledAssessments().keySet()) {
                    if (!assessmentIdsForStudent.contains(assess.getId())) {
                        clonedMap.remove(assess);
                    }
                }
                entry.getValue().setScheduledAssessments(clonedMap);
            }
        }

        return studentsScheduledMap;
    }

    private int countStudentsToSchedule(Map<String, ScheduledStudent> studentsScheduled) {

        int count = 0;

        for (ScheduledStudent student : studentsScheduled.values()) {
            if (!student.isStudentFullyScheduled()) {
                count++;
            }
        }

        return count;
    }

    private Map<String, ScheduledStudent> filterOnlyUnscheduledStudents(
            final Map<String, ScheduledStudent> studentsScheduled, final TreeSet<ScheduledTimeSlot> timeSlots) {

        // lookup in studentTestReport by student id, state, and assessment id
        // if the record is found and status is completed, then filter out

        // if the student is in the schedule prior to "now" and has a status of started in the studentTestReport
        // collection, then student
        // is eligible for rescheduling

        // if the student is in the schedule after "now" then filter student out

        // if student is not in the schedule at all, then student is eligible for rescheduling

        for (ScheduledStudent scheduledStudent : studentsScheduled.values()) {

            // TODO add check for student test report, cannot do this until new code is committed

            // get all reports for student id, state abbr

            for (Assessment assessment : scheduledStudent.getScheduledAssessments().keySet()) {
                // find assessment in report list for opportunity 1, if found and status is COMPLETED then
                // set value for assessment to TRUE

                ScheduledTimeSlot timeSlot = findSlotForScheduledAssessment(timeSlots, assessment);

                if (timeSlot.getStartTime().isEqualNow() || timeSlot.getStartTime().isAfterNow()) {
                    scheduledStudent.setAssessmentScheduled(assessment);
                }

            }

        }

        return studentsScheduled;
    }

    private ScheduledTimeSlot findSlotForScheduledAssessment(final TreeSet<ScheduledTimeSlot> timeSlots,
            final Assessment assessment) {

        for (ScheduledTimeSlot timeSlot : timeSlots) {
            for (ScheduledSeat seat : timeSlot.getSeats()) {
                if (seat.getAssessment().equals(assessment)) {
                    return timeSlot;
                }
            }
        }

        return null;
    }

    private HashMultimap<String, Assessment> createGradesToAssessmentsMap(
            final Map<String, ScheduledStudent> studentsScheduled) {

        HashMultimap<String, Assessment> gradesToAssessments = HashMultimap.create();

        for (ScheduledStudent scheduledStudent : studentsScheduled.values()) {
            String grade = scheduledStudent.getStudent().getGradeLevelWhenAssessed().toString();

            for (Assessment assessment : scheduledStudent.getScheduledAssessments().keySet()) {
                gradesToAssessments.put(grade, assessment);
            }
        }

        return gradesToAssessments;
    }

    private HashMultimap<String, Assessment> createStudentGroupToAssessmentMap(
            Map<String, ScheduledStudent> studentsScheduled, String institutionMongoId) {
        // find all the available student groups
        List<StudentGroup> sgroups = schedulerHelper.findStudentGroups(institutionMongoId);
        HashMultimap<String, Assessment> sgToAssessments = HashMultimap.create();
        for (StudentGroup sg : sgroups) {
            // find the students who belong to this group and in the ready for scheduling
            for (String studentId : sg.getStudentIds()) {
                // find if this student exists in the studentsScheduledMap
                ScheduledStudent student = studentsScheduled.get(studentId);
                if (student != null && !student.getScheduledAssessments().isEmpty()) {
                    for (Assessment assessment : student.getScheduledAssessments().keySet()) {
                        sgToAssessments.put(sg.getId(), assessment);
                    }
                }
            }
        }
        return sgToAssessments;

    }

    private List<String> getTenantIds(final String institutionId) {

        List<Tenant> tenantsForAssessment = Lists.newArrayList();
        final List<String> tenantIdsForAssessment = Lists.newArrayList();

        Sb11Entity resEntity = testRegPersister.findById(institutionId, InstitutionEntity.FORMAT_TYPE);
        final Set<SbacEntity> possibleTenants = Sets.newHashSet(
                new SbacEntity(resEntity.getTenantType(), resEntity.getEntityId(), resEntity.getEntityName()));

        while (resEntity != null) {
            if (resEntity.getParentEntityType() != null) {
                final TenantType tenantTypes = convertHierarchyLevelToTenantType(resEntity.getParentEntityType());
                if (tenantTypes != null) {
                    possibleTenants.add(new SbacEntity(tenantTypes, resEntity.getParentEntityId(), "nothing"));
                }
            }
            resEntity = resEntity.getParentEntityId() != null ? this.sb11EntityService.getParentEntity(resEntity)
                    : null;
        }

        if (!CollectionUtils.isEmpty(possibleTenants)) {
            tenantsForAssessment = this.tenancyService.getApplicableTenants(possibleTenants);
        }

        if (!CollectionUtils.isEmpty(tenantsForAssessment)) {
            tenantIdsForAssessment.addAll(Lists.transform(tenantsForAssessment, ARTHelpers.TENANT_ID_TRANSFORMER));
        }

        return tenantIdsForAssessment;
    }

    private TenantType convertHierarchyLevelToTenantType(final HierarchyLevel level) {
        TenantType converted = null;

        switch (level) {
        case DISTRICT:
            converted = TenantType.DISTRICT;
            break;
        case GROUPOFDISTRICTS:
            converted = TenantType.DISTRICT_GROUP;
            break;
        case GROUPOFINSTITUTIONS:
            converted = TenantType.INSTITUTION_GROUP;
            break;
        case GROUPOFSTATES:
            converted = TenantType.STATE_GROUP;
            break;
        case STATE:
            converted = TenantType.STATE;
            break;
        default:
            converted = null;
            break;
        }

        return converted;
    }
}