Java tutorial
/* 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; } }