org.unitime.timetable.onlinesectioning.custom.purdue.XEBatchSolverSaver.java Source code

Java tutorial

Introduction

Here is the source code for org.unitime.timetable.onlinesectioning.custom.purdue.XEBatchSolverSaver.java

Source

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation 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:
 *
 * 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.unitime.timetable.onlinesectioning.custom.purdue;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cpsolver.coursett.model.TimeLocation;
import org.cpsolver.ifs.solver.Solver;
import org.cpsolver.ifs.util.Progress;
import org.cpsolver.ifs.util.CSVFile.CSVField;
import org.cpsolver.studentsct.StudentSectioningSaver;
import org.cpsolver.studentsct.model.CourseRequest;
import org.cpsolver.studentsct.model.Enrollment;
import org.cpsolver.studentsct.model.Request;
import org.cpsolver.studentsct.model.SctAssignment;
import org.cpsolver.studentsct.model.Section;
import org.cpsolver.studentsct.model.Student;
import org.cpsolver.studentsct.reservation.LearningCommunityReservation;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.Transaction;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.restlet.Client;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.MediaType;
import org.restlet.data.Protocol;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;
import org.unitime.timetable.ApplicationProperties;
import org.unitime.timetable.defaults.ApplicationProperty;
import org.unitime.timetable.gwt.shared.SectioningException;
import org.unitime.timetable.interfaces.ExternalClassLookupInterface;
import org.unitime.timetable.model.Class_;
import org.unitime.timetable.model.CourseOffering;
import org.unitime.timetable.model.OfferingConsentType;
import org.unitime.timetable.model.Session;
import org.unitime.timetable.model.dao.SessionDAO;
import org.unitime.timetable.model.dao._RootDAO;
import org.unitime.timetable.onlinesectioning.AcademicSessionInfo;
import org.unitime.timetable.onlinesectioning.OnlineSectioningHelper;
import org.unitime.timetable.onlinesectioning.OnlineSectioningLog;
import org.unitime.timetable.onlinesectioning.OnlineSectioningLogger;
import org.unitime.timetable.onlinesectioning.OnlineSectioningLog.Entity;
import org.unitime.timetable.onlinesectioning.OnlineSectioningServer;
import org.unitime.timetable.onlinesectioning.custom.CustomStudentEnrollmentHolder;
import org.unitime.timetable.onlinesectioning.custom.ExternalTermProvider;
import org.unitime.timetable.onlinesectioning.model.XStudent;
import org.unitime.timetable.solver.studentsct.InMemoryReport;
import org.unitime.timetable.solver.studentsct.StudentSolver;
import org.unitime.timetable.util.Constants;
import org.unitime.timetable.util.DefaultExternalClassLookup;
import org.unitime.timetable.util.Formats;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
 * @author Tomas Muller
 */
public class XEBatchSolverSaver extends StudentSectioningSaver {
    private static Log sLog = LogFactory.getLog(XEBatchSolverSaver.class);
    private String iInitiative = null;
    private String iTerm = null;
    private String iYear = null;
    private String iOwnerId = null;
    private Progress iProgress = null;

    private Client iClient;
    private ExternalTermProvider iExternalTermProvider;
    private ExternalClassLookupInterface iExternalClassLookup;
    private AcademicSessionInfo iSession;
    private String iHoldPassword = null;
    private String iRegistrationDate = null;
    private String iActionAdd = null, iActionDrop = null;
    private boolean iConditionalAddDrop = true;
    private InMemoryReport iCSV;
    private boolean iAutoOverrides = false;
    private Set<String> iAllowedOverrides = new HashSet<String>();
    private int iNrThreads = 1;
    private boolean iCanContinue = true;
    private boolean iTimeConflictsIgnoreBreakTimes = false;
    private boolean iAutoTimeOverrides = false;
    private boolean iAutoLCOverrides = false;

    private Hashtable<Long, CourseOffering> iCourses = null;
    private Hashtable<Long, Class_> iClasses = null;
    private List<XStudent> iUpdatedStudents = new ArrayList<XStudent>();

    public XEBatchSolverSaver(Solver solver) {
        super(solver);
        iInitiative = solver.getProperties().getProperty("Data.Initiative");
        iYear = solver.getProperties().getProperty("Data.Year");
        iTerm = solver.getProperties().getProperty("Data.Term");
        iOwnerId = solver.getProperties().getProperty("General.OwnerPuid");
        iProgress = Progress.getInstance(getModel());
        try {
            String clazz = ApplicationProperty.CustomizationExternalTerm.value();
            if (clazz == null || clazz.isEmpty())
                iExternalTermProvider = new BannerTermProvider();
            else
                iExternalTermProvider = (ExternalTermProvider) Class.forName(clazz).getConstructor().newInstance();
        } catch (Exception e) {
            sLog.error("Failed to create external term provider, using the default one instead.", e);
            iExternalTermProvider = new BannerTermProvider();
        }
        try {
            String clazz = ApplicationProperty.CustomizationExternalClassLookup.value();
            if (clazz == null || clazz.isEmpty())
                iExternalClassLookup = new DefaultExternalClassLookup();
            else
                iExternalClassLookup = (ExternalClassLookupInterface) Class.forName(clazz).getConstructor()
                        .newInstance();
        } catch (Exception e) {
            sLog.error("Failed to create external class lookup, using the default one instead.", e);
            iExternalClassLookup = new DefaultExternalClassLookup();
        }
        iHoldPassword = solver.getProperties().getProperty("Save.XE.HoldPassword");
        iRegistrationDate = solver.getProperties().getProperty("Save.XE.RegistrationDate");
        iActionAdd = solver.getProperties().getProperty("Save.XE.ActionAdd", "RE");
        iActionDrop = solver.getProperties().getProperty("Save.XE.ActionDrop", "DDD");
        iConditionalAddDrop = solver.getProperties().getPropertyBoolean("Save.XE.ConditionalAddDrop", true);
        iAutoOverrides = solver.getProperties().getPropertyBoolean("Save.XE.AutoOverrides", false);
        String allowedOverrides = solver.getProperties().getProperty("Save.XE.AllowedOverrides", null);
        if (allowedOverrides != null && !allowedOverrides.isEmpty())
            iAllowedOverrides = new HashSet<String>(Arrays.asList(allowedOverrides.split(",")));
        iAutoTimeOverrides = solver.getProperties().getPropertyBoolean("Save.XE.AutoTimeOverrides",
                iAutoOverrides && iAllowedOverrides.contains("TIME-CNFLT"));
        iAutoLCOverrides = solver.getProperties().getPropertyBoolean("Save.XE.AutoLCOverrides", false);
        iTimeConflictsIgnoreBreakTimes = solver.getProperties()
                .getPropertyBoolean("Save.XE.TimeConflictsIgnoreBreakTimes", false);
        iNrThreads = solver.getProperties().getPropertyInt("Save.XE.NrSaveThreads", 10);
        iCSV = new InMemoryReport("XE", "Last XE Enrollment Results ("
                + Formats.getDateFormat(Formats.Pattern.DATE_TIME_STAMP_SHORT).format(new Date()) + ")");
        ((StudentSolver) solver).setReport(iCSV);
    }

    @Override
    public void save() throws Exception {
        iProgress.setStatus("Saving solution ...");
        List<Protocol> protocols = new ArrayList<Protocol>();
        protocols.add(Protocol.HTTP);
        protocols.add(Protocol.HTTPS);
        iClient = new Client(protocols);
        iCSV.setHeader(new CSVField[] { new CSVField("PUID"), new CSVField("Name"), new CSVField("Course"),
                new CSVField("CRN"), new CSVField("Request"), new CSVField("Status"), new CSVField("Message"),
                new CSVField("Used Override") });
        org.hibernate.Session hibSession = null;
        Transaction tx = null;
        try {
            hibSession = SessionDAO.getInstance().getSession();
            hibSession.setCacheMode(CacheMode.IGNORE);
            hibSession.setFlushMode(FlushMode.MANUAL);

            tx = hibSession.beginTransaction();

            Session session = Session.getSessionUsingInitiativeYearTerm(iInitiative, iYear, iTerm);
            if (session == null)
                throw new Exception("Session " + iInitiative + " " + iTerm + iYear + " not found!");
            ApplicationProperties.setSessionId(session.getUniqueId());
            iSession = new AcademicSessionInfo(session);

            save(session, hibSession);

            if (!iUpdatedStudents.isEmpty() && CustomStudentEnrollmentHolder.isCanRequestUpdates()) {
                CustomStudentEnrollmentHolder.getProvider().requestUpdate((OnlineSectioningServer) getSolver(),
                        new OnlineSectioningHelper(hibSession, getUser()), iUpdatedStudents);
            }

            hibSession.flush();

            tx.commit();
            tx = null;
        } catch (Exception e) {
            iProgress.fatal("Unable to save , reason: " + e.getMessage(), e);
            sLog.error(e.getMessage(), e);
            if (tx != null)
                tx.rollback();
        } finally {
            if (hibSession != null && hibSession.isOpen())
                hibSession.close();
            try {
                iClient.stop();
            } catch (Exception e) {
                sLog.error(e.getMessage(), e);
            }
        }
    }

    public void save(Session session, org.hibernate.Session hibSession) {
        setPhase("Loading classes...", 1);
        iClasses = new Hashtable<Long, Class_>();
        for (Class_ clazz : (List<Class_>) hibSession.createQuery("select distinct c from Class_ c where "
                + "c.schedulingSubpart.instrOfferingConfig.instructionalOffering.session.uniqueId = :sessionId")
                .setLong("sessionId", session.getUniqueId()).list()) {
            iClasses.put(clazz.getUniqueId(), clazz);
        }
        incProgress();

        iCourses = new Hashtable<Long, CourseOffering>();
        setPhase("Loading courses...", 1);
        for (CourseOffering course : (List<CourseOffering>) hibSession
                .createQuery(
                        "select distinct c from CourseOffering c where c.subjectArea.session.uniqueId = :sessionId")
                .setLong("sessionId", session.getUniqueId()).list()) {
            iCourses.put(course.getUniqueId(), course);
        }
        incProgress();

        setPhase("Enrolling students...", getModel().getStudents().size());
        if (iNrThreads <= 1) {
            for (Student student : getModel().getStudents()) {
                incProgress();
                if (student.isDummy())
                    continue;
                saveStudent(student);
            }
        } else {
            List<Worker> workers = new ArrayList<Worker>();
            Iterator<Student> students = getModel().getStudents().iterator();
            for (int i = 0; i < iNrThreads; i++)
                workers.add(new Worker(i, students));
            for (Worker worker : workers)
                worker.start();
            for (Worker worker : workers) {
                try {
                    worker.join();
                } catch (InterruptedException e) {
                    iCanContinue = false;
                    try {
                        worker.join();
                    } catch (InterruptedException x) {
                    }
                }
            }
            if (!iCanContinue)
                throw new RuntimeException("The save was interrupted.");
        }
    }

    protected void saveStudent(Student student) {
        long c0 = OnlineSectioningHelper.getCpuTime();
        OnlineSectioningLog.Action.Builder action = OnlineSectioningLog.Action.newBuilder();
        action.setOperation("batch-enroll");
        action.setSession(OnlineSectioningLog.Entity.newBuilder().setUniqueId(iSession.getUniqueId())
                .setName(iSession.toCompactString()));
        action.setStartTime(System.currentTimeMillis());
        action.setUser(getUser());
        action.setStudent(OnlineSectioningLog.Entity.newBuilder().setUniqueId(student.getId())
                .setExternalId(student.getExternalId()).setName(student.getName())
                .setType(OnlineSectioningLog.Entity.EntityType.STUDENT));
        OnlineSectioningLog.Enrollment.Builder requested = OnlineSectioningLog.Enrollment.newBuilder();
        requested.setType(OnlineSectioningLog.Enrollment.EnrollmentType.REQUESTED);
        for (Request request : student.getRequests()) {
            action.addRequest(OnlineSectioningHelper.toProto(request));
            if (request instanceof CourseRequest) {
                Enrollment e = getAssignment().getValue(request);
                if (e != null)
                    for (Section section : e.getSections())
                        requested.addSection(OnlineSectioningHelper.toProto(section, e));
            }
        }
        action.addEnrollment(requested);
        List<CSVField[]> csv = new ArrayList<CSVField[]>();
        try {
            enroll(student, getCrns(student), getLCCrns(student), action, csv);
        } catch (Exception e) {
            if (e instanceof SectioningException) {
                if (e.getCause() == null) {
                    iProgress.info("Enrollment failed: " + e.getMessage());
                } else {
                    iProgress.warn("Enrollment failed: " + e.getMessage(), e.getCause());
                }
            } else {
                iProgress.error("Enrollment failed: " + e.getMessage(), e);
            }
            String puid = getBannerId(student);
            for (String id : getCrns(student)) {
                if (id == null)
                    continue;
                csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                        new CSVField(getCourseNameForCrn(student, id)), new CSVField(id), new CSVField("Add"),
                        new CSVField("Failed"), new CSVField(e.getMessage()) });
            }
            action.setResult(OnlineSectioningLog.Action.ResultType.FAILURE);
            if (e.getMessage() != null)
                action.setApiException(e.getMessage());
            if (e.getCause() != null && e instanceof SectioningException)
                action.addMessage(
                        OnlineSectioningLog.Message.newBuilder().setLevel(OnlineSectioningLog.Message.Level.FATAL)
                                .setText(e.getCause().getClass().getName() + ": " + e.getCause().getMessage()));
            else
                action.addMessage(
                        OnlineSectioningLog.Message.newBuilder().setLevel(OnlineSectioningLog.Message.Level.FATAL)
                                .setText(e.getMessage() == null ? "null" : e.getMessage()));
        } finally {
            action.setEndTime(System.currentTimeMillis()).setCpuTime(OnlineSectioningHelper.getCpuTime() - c0);
        }
        StringBuffer table = new StringBuffer();
        synchronized (iCSV) {
            for (CSVField[] line : csv) {
                if (table.length() > 0)
                    table.append("\n");
                table.append(iCSV.addLine(line));
            }
            action.addOptionBuilder().setKey("table").setValue(table.toString());
            iUpdatedStudents.add(new XStudent(student, getAssignment()));
        }
        OnlineSectioningLogger.getInstance().record(OnlineSectioningLog.Log.newBuilder().addAction(action).build());
    }

    protected Set<String> getCrns(Student student) {
        Set<String> crns = new TreeSet<String>();
        for (Request request : student.getRequests()) {
            Enrollment enrollment = getAssignment().getValue(request);
            if (enrollment != null && enrollment.isCourseRequest()) {
                CourseOffering course = iCourses.get(enrollment.getCourse().getId());
                for (Section section : enrollment.getSections()) {
                    Class_ clazz = iClasses.get(section.getId());
                    if (clazz != null && course != null)
                        crns.add(clazz.getExternalId(course));
                }
            }
        }
        return crns;
    }

    protected Set<String> getLCCrns(Student student) {
        Set<String> crns = new TreeSet<String>();
        if (!iAutoLCOverrides)
            return crns;
        for (Request request : student.getRequests()) {
            Enrollment enrollment = getAssignment().getValue(request);
            if (enrollment != null && enrollment.isCourseRequest() && enrollment.getReservation() != null
                    && enrollment.getReservation() instanceof LearningCommunityReservation) {
                CourseOffering course = iCourses.get(enrollment.getCourse().getId());
                for (Section section : enrollment.getSections()) {
                    Class_ clazz = iClasses.get(section.getId());
                    if (clazz != null && course != null)
                        crns.add(clazz.getExternalId(course));
                }
            }
        }
        return crns;
    }

    protected String getCrn(Enrollment enrollment, Section section) {
        CourseOffering course = iCourses.get(enrollment.getCourse().getId());
        Class_ clazz = iClasses.get(section.getId());
        if (clazz != null && course != null)
            return clazz.getExternalId(course);
        return null;
    }

    protected boolean shareHoursIgnoreBreakTime(TimeLocation t1, TimeLocation t2) {
        int s1 = t1.getStartSlot() * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
        int e1 = (t1.getStartSlot() + t1.getLength()) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN
                - t1.getBreakTime();
        int s2 = t2.getStartSlot() * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
        int e2 = (t2.getStartSlot() + t2.getLength()) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN
                - t2.getBreakTime();
        return e1 > s2 && e2 > s1;
    }

    protected boolean inConflict(SctAssignment a1, SctAssignment a2) {
        if (a1.getTime() == null || a2.getTime() == null)
            return false;
        if (iTimeConflictsIgnoreBreakTimes) {
            TimeLocation t1 = a1.getTime();
            TimeLocation t2 = a2.getTime();
            return t1.shareDays(t2) && shareHoursIgnoreBreakTime(t1, t2) && t1.shareWeeks(t2);
        } else {
            return a1.getTime().hasIntersection(a2.getTime());
        }
    }

    protected Set<String> getTimeConflicts(Student student) {
        Set<String> crns = new TreeSet<String>();
        for (int i1 = 0; i1 < student.getRequests().size(); i1++) {
            Request r1 = student.getRequests().get(i1);
            Enrollment e1 = getAssignment().getValue(r1);
            if (e1 != null && e1.isCourseRequest()) {
                for (int i2 = i1 + 1; i2 < student.getRequests().size(); i2++) {
                    Request r2 = student.getRequests().get(i2);
                    Enrollment e2 = getAssignment().getValue(r2);
                    if (e2 != null && e2.isCourseRequest()) {
                        for (Section s1 : e1.getSections()) {
                            for (Section s2 : e2.getSections()) {
                                if (inConflict(s1, s2)) {
                                    if (s1.isAllowOverlap()) {
                                        String crn = getCrn(e1, s1);
                                        if (crn != null)
                                            crns.add(crn);
                                    } else if (s2.isAllowOverlap()) {
                                        String crn = getCrn(e2, s2);
                                        if (crn != null)
                                            crns.add(crn);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return crns;
    }

    protected Gson getGson() {
        GsonBuilder builder = new GsonBuilder().registerTypeAdapter(DateTime.class, new JsonSerializer<DateTime>() {
            @Override
            public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) {
                return new JsonPrimitive(src.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"));
            }
        }).registerTypeAdapter(DateTime.class, new JsonDeserializer<DateTime>() {
            @Override
            public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {
                return new DateTime(json.getAsJsonPrimitive().getAsString(), DateTimeZone.UTC);
            }
        });
        return builder.create();
    }

    protected void enroll(Student student, Set<String> crns, Set<String> lcCrns,
            OnlineSectioningLog.Action.Builder action, List<CSVField[]> csv) throws IOException {
        iProgress.info("[" + student.getExternalId() + "] " + student.getName() + " " + crns);

        ClientResource resource = null;
        try {
            resource = new ClientResource(ApplicationProperties.getProperty("banner.xe.site"));
            resource.setNext(iClient);
            resource.setChallengeResponse(ChallengeScheme.HTTP_BASIC,
                    ApplicationProperties.getProperty("banner.xe.admin.user"),
                    ApplicationProperties.getProperty("banner.xe.admin.password"));

            String term = iExternalTermProvider.getExternalTerm(iSession);
            String campus = iExternalTermProvider.getExternalCampus(iSession);
            String puid = getBannerId(student);

            resource.addQueryParameter("term", term);
            resource.addQueryParameter("bannerId", puid);
            resource.addQueryParameter("systemIn", "SB");
            resource.addQueryParameter("persona", "SB");
            if (iHoldPassword != null && !iHoldPassword.isEmpty())
                resource.addQueryParameter("holdPassword", iHoldPassword);
            action.addOptionBuilder().setKey("term").setValue(term);
            action.addOptionBuilder().setKey("bannerId").setValue(getBannerId(student));
            Gson gson = getGson();

            long t0 = System.currentTimeMillis();
            XEInterface.RegisterResponse original = null;
            try {
                original = getSchedule(student, resource);
            } finally {
                action.setApiGetTime(System.currentTimeMillis() - t0);
            }
            action.addOptionBuilder().setKey("original").setValue(gson.toJson(original));

            Set<String> noadd = new HashSet<String>();
            Set<String> nodrop = new HashSet<String>();
            Set<String> notregistered = new HashSet<String>();
            Map<String, XEInterface.Registration> registered = new HashMap<String, XEInterface.Registration>();
            if (original.registrations != null) {
                OnlineSectioningLog.Enrollment.Builder previous = OnlineSectioningLog.Enrollment.newBuilder();
                previous.setType(OnlineSectioningLog.Enrollment.EnrollmentType.PREVIOUS);
                for (XEInterface.Registration reg : original.registrations) {
                    if (reg.isRegistered()) {
                        registered.put(reg.courseReferenceNumber, reg);
                        if (!reg.can(iActionDrop))
                            nodrop.add(reg.courseReferenceNumber);
                        previous.addSectionBuilder()
                                .setClazz(
                                        OnlineSectioningLog.Entity.newBuilder().setName(reg.courseReferenceNumber))
                                .setCourse(OnlineSectioningLog.Entity.newBuilder()
                                        .setName(reg.subject + " " + reg.courseNumber))
                                .setSubpart(OnlineSectioningLog.Entity.newBuilder().setName(reg.scheduleType));
                    } else {
                        notregistered.add(reg.courseReferenceNumber);
                        if (!reg.can(iActionAdd))
                            noadd.add(reg.courseReferenceNumber);
                    }
                }
                action.addEnrollment(previous);
            }

            action.setResult(OnlineSectioningLog.Action.ResultType.TRUE);
            Set<String> added = new HashSet<String>();
            XEInterface.RegisterRequest req = new XEInterface.RegisterRequest(term, puid, null, true);
            if (iHoldPassword != null && !iHoldPassword.isEmpty())
                req.holdPassword = iHoldPassword;
            if (iRegistrationDate != null && !iRegistrationDate.isEmpty())
                req.registrationDate = iRegistrationDate;
            if (iConditionalAddDrop)
                req.conditionalAddDrop = "Y";

            for (String id : crns) {
                if (id == null)
                    continue;
                if (!registered.containsKey(id) && noadd.contains(id)) {
                    csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                            new CSVField(getCourseNameForCrn(student, id)), new CSVField(id), new CSVField("Add"),
                            new CSVField("Failed"), new CSVField("Action " + iActionAdd + " is not allowed.") });
                    iProgress.warn("[" + student.getExternalId() + "] " + id + ": Action " + iActionAdd
                            + " is not allowed.");
                    action.setResult(OnlineSectioningLog.Action.ResultType.FALSE);
                    action.addMessage(OnlineSectioningLog.Message.newBuilder()
                            .setLevel(OnlineSectioningLog.Message.Level.WARN)
                            .setText(id + ": Action " + iActionAdd + " is not allowed."));
                } else {
                    if (registered.containsKey(id)) {
                        if (added.add(id))
                            keep(req, id);
                    } else {
                        if (added.add(id))
                            add(req, id, notregistered.contains(id));
                    }
                }
            }
            Set<String> dropped = new HashSet<String>();
            for (String id : registered.keySet()) {
                if (added.contains(id))
                    continue;
                XEInterface.Registration reg = registered.get(id);
                if (!campus.equals(reg.campus)
                        && iExternalClassLookup.findCourseByExternalId(iSession.getUniqueId(), id) == null) {
                    if (added.add(id))
                        keep(req, id);
                } else if (nodrop.contains(id)) {
                    csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                            new CSVField(reg.subject + " " + reg.courseNumber), new CSVField(id),
                            new CSVField("Drop"), new CSVField("Failed"),
                            new CSVField("Action " + iActionDrop + " is not allowed.") });
                    iProgress.warn("[" + student.getExternalId() + "] " + id + ": Action " + iActionDrop
                            + " is not allowed.");
                    action.setResult(OnlineSectioningLog.Action.ResultType.FALSE);
                    action.addMessage(OnlineSectioningLog.Message.newBuilder()
                            .setLevel(OnlineSectioningLog.Message.Level.WARN)
                            .setText(id + ": Action " + iActionDrop + " is not allowed."));
                    if (added.add(id))
                        keep(req, id);
                } else {
                    drop(req, id);
                    dropped.add(id);
                }
            }
            Map<String, Set<String>> appliedOverrides = new HashMap<String, Set<String>>();

            if (iAutoTimeOverrides) {
                for (String crn : getTimeConflicts(student))
                    addOverride(student, req, crn, "TIME-CNFLT", appliedOverrides);
            }

            action.addOptionBuilder().setKey("request").setValue(gson.toJson(req));
            long t1 = System.currentTimeMillis();
            XEInterface.RegisterResponse response = null;
            try {
                response = postChanges(resource, req);
            } finally {
                action.setApiPostTime(System.currentTimeMillis() - t1);
            }
            action.addOptionBuilder().setKey("response").setValue(gson.toJson(response));

            int index = 1;
            while (iAutoOverrides && response.registrations != null) {
                boolean changed = false;
                for (XEInterface.Registration reg : response.registrations) {
                    String id = reg.courseReferenceNumber;
                    if (reg.crnErrors != null && "F".equals(reg.statusIndicator)) {
                        for (XEInterface.CrnError e : reg.crnErrors) {
                            String override = getDefaultOverride(student, id, e.messageType);
                            if (override != null && (iAllowedOverrides.contains(override) || lcCrns.contains(id))) {
                                if (addOverride(student, req, id, override, appliedOverrides)) {
                                    changed = true;
                                    break;
                                }
                            }
                        }
                    }
                    if (!iConditionalAddDrop && dropped.contains(reg.courseReferenceNumber)
                            && ("Deleted".equals(reg.statusDescription)
                                    || "Dropped".equals(reg.statusDescription))) {
                        removeAction(req, reg.courseReferenceNumber);
                    }
                }
                if (!changed)
                    break;
                action.addOptionBuilder().setKey("request-override-" + index).setValue(gson.toJson(req));
                long t2 = System.currentTimeMillis();
                try {
                    response = postChanges(resource, req);
                } finally {
                    action.setApiPostTime(System.currentTimeMillis() - t2 + action.getApiPostTime());
                }
                action.addOptionBuilder().setKey("response-override-" + index).setValue(gson.toJson(response));
                index++;
            }

            Set<String> checked = new HashSet<String>();
            if (response.registrations != null) {
                OnlineSectioningLog.Enrollment.Builder stored = OnlineSectioningLog.Enrollment.newBuilder();
                stored.setType(OnlineSectioningLog.Enrollment.EnrollmentType.STORED);
                for (XEInterface.Registration reg : response.registrations) {
                    if ("Registered".equals(reg.statusDescription)) {
                        stored.addSectionBuilder()
                                .setClazz(
                                        OnlineSectioningLog.Entity.newBuilder().setName(reg.courseReferenceNumber))
                                .setCourse(OnlineSectioningLog.Entity.newBuilder()
                                        .setName(reg.subject + " " + reg.courseNumber))
                                .setSubpart(OnlineSectioningLog.Entity.newBuilder().setName(reg.scheduleType));
                    }
                    String id = reg.courseReferenceNumber;
                    checked.add(id);
                    String op = (added.contains(id) ? "Add" : "Drop");
                    if (notregistered.contains(id))
                        continue;
                    String error = null;
                    if (reg.crnErrors != null && !reg.crnErrors.isEmpty())
                        for (XEInterface.CrnError e : reg.crnErrors) {
                            if (error == null)
                                error = e.messageType + ": " + e.message;
                            else
                                error += "\n" + e.messageType + ": " + e.message;
                            ;
                        }
                    csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                            new CSVField(reg.subject + " " + reg.courseNumber), new CSVField(id), new CSVField(op),
                            new CSVField(reg.statusDescription), new CSVField(error),
                            new CSVField(getOverride(req, id, appliedOverrides)) });
                    if (error != null) {
                        if ("F".equals(reg.statusIndicator)) {
                            iProgress.warn("[" + student.getExternalId() + "] " + id + ": " + error);
                            action.setResult(OnlineSectioningLog.Action.ResultType.FALSE);
                            action.addMessage(OnlineSectioningLog.Message.newBuilder()
                                    .setLevel(OnlineSectioningLog.Message.Level.WARN).setText(id + ": " + error));
                        } else {
                            iProgress.info("[" + student.getExternalId() + "] " + id + ": " + error);
                            action.addMessage(OnlineSectioningLog.Message.newBuilder()
                                    .setLevel(OnlineSectioningLog.Message.Level.INFO).setText(id + ": " + error));
                        }
                    }
                }
                action.addEnrollment(stored);
            }

            if (response.failedRegistrations != null) {
                for (XEInterface.FailedRegistration reg : response.failedRegistrations) {
                    if (reg.failedCRN == null || reg.failure == null)
                        continue;
                    String id = reg.failedCRN;
                    checked.add(id);
                    String op = (added.contains(id) ? "Add" : "Drop");
                    String error = reg.failure;
                    csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                            new CSVField(getCourseNameForCrn(student, id)), new CSVField(id), new CSVField(op),
                            new CSVField("Failed"), new CSVField(error),
                            new CSVField(getOverride(req, id, appliedOverrides)) });
                    iProgress.warn("[" + student.getExternalId() + "] " + id + ": " + error);
                    action.setResult(OnlineSectioningLog.Action.ResultType.FALSE);
                    action.addMessage(OnlineSectioningLog.Message.newBuilder()
                            .setLevel(OnlineSectioningLog.Message.Level.WARN).setText(id + ": " + error));
                }
            }

            boolean ex = false;
            for (String id : crns) {
                if (id == null)
                    continue;
                if (checked.contains(id))
                    continue;
                String op = (added.contains(id) ? "Add" : "Drop");
                ex = true;
                csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()),
                        new CSVField(getCourseNameForCrn(student, id)), new CSVField(id), new CSVField(op),
                        new CSVField("Exception"), new CSVField(response.registrationException) });
            }

            if (response.registrationException != null && !ex) {
                csv.add(new CSVField[] { new CSVField(puid), new CSVField(student.getName()), new CSVField(null),
                        new CSVField(null), new CSVField(null), new CSVField("Exception"),
                        new CSVField(response.registrationException) });
            }

            if (response.registrationException != null) {
                action.setResult(OnlineSectioningLog.Action.ResultType.FAILURE);
                action.addMessage(OnlineSectioningLog.Message.newBuilder()
                        .setLevel(OnlineSectioningLog.Message.Level.ERROR).setText(response.registrationException));
            }

            if (response.registrationException != null)
                iProgress.warn("[" + student.getExternalId() + "] " + response.registrationException);
        } finally {
            if (resource != null) {
                if (resource.getResponse() != null)
                    resource.getResponse().release();
                resource.release();
            }
        }
    }

    protected boolean addOverride(Student student, XEInterface.RegisterRequest req, String id, String override,
            Map<String, Set<String>> overrides) {
        if (req.courseReferenceNumbers == null)
            return false;
        for (XEInterface.CourseReferenceNumber crn : req.courseReferenceNumbers) {
            if (id.equals(crn.courseReferenceNumber)) {
                iProgress
                        .debug("[" + student.getExternalId() + "] " + "Adding override " + override + " for " + id);
                crn.courseOverride = override;
                Set<String> list = overrides.get(id);
                if (list == null) {
                    list = new TreeSet<String>();
                    overrides.put(id, list);
                }
                return list.add(override);
            }
        }
        iProgress.warn("[" + student.getExternalId() + "] " + "Failed to add override " + override + " for " + id);
        return false;
    }

    protected String getOverride(XEInterface.RegisterRequest req, String id, Map<String, Set<String>> overrides) {
        Set<String> list = overrides.get(id);
        if (list != null) {
            String ret = "";
            for (String override : list)
                ret += (ret.isEmpty() ? "" : ",") + override;
            return ret;
        }
        if (req.courseReferenceNumbers != null)
            for (XEInterface.CourseReferenceNumber crn : req.courseReferenceNumbers)
                if (id.equals(crn.courseReferenceNumber))
                    return crn.courseOverride;
        return null;
    }

    protected XEInterface.RegisterResponse getSchedule(Student student, ClientResource resource)
            throws IOException {
        try {
            resource.get(MediaType.APPLICATION_JSON);
        } catch (ResourceException e) {
            handleError(resource, e);
        }

        List<XEInterface.RegisterResponse> current = new GsonRepresentation<List<XEInterface.RegisterResponse>>(
                resource.getResponseEntity(), XEInterface.RegisterResponse.TYPE_LIST).getObject();
        XEInterface.RegisterResponse original = null;
        if (current != null && !current.isEmpty())
            original = current.get(0);

        if (original == null || !original.validStudent) {
            String reason = null;
            if (original != null && original.failureReasons != null) {
                for (String m : original.failureReasons) {
                    if ("Holds prevent registration.".equals(m) && iHoldPassword != null
                            && !iHoldPassword.isEmpty())
                        return getHoldSchedule(student, resource);
                    if ("Invalid or undefined Enrollment Status or date range invalid.".equals(m)
                            && iRegistrationDate != null && !iRegistrationDate.isEmpty())
                        return getHoldSchedule(student, resource);
                    if (m != null)
                        reason = m;
                }
            }
            if (reason != null)
                throw new SectioningException(reason);
            throw new SectioningException("Failed to check student registration status.");
        }

        return original;
    }

    protected XEInterface.RegisterResponse getHoldSchedule(Student student, ClientResource resource)
            throws IOException {
        if (iHoldPassword != null && !iHoldPassword.isEmpty())
            iProgress.debug("[" + student.getExternalId() + "] " + "Using hold password...");
        if (iRegistrationDate != null && !iRegistrationDate.isEmpty())
            iProgress.debug("[" + student.getExternalId() + "] " + "Using registration date...");

        XEInterface.RegisterRequest req = new XEInterface.RegisterRequest(resource.getQueryValue("term"),
                resource.getQueryValue("bannerId"), null, true);
        req.empty();
        if (iHoldPassword != null && !iHoldPassword.isEmpty())
            req.holdPassword = iHoldPassword;
        if (iRegistrationDate != null && !iRegistrationDate.isEmpty())
            req.registrationDate = iRegistrationDate;
        try {
            resource.post(new GsonRepresentation<XEInterface.RegisterRequest>(req));
        } catch (ResourceException e) {
            handleError(resource, e);
        }

        XEInterface.RegisterResponse response = new GsonRepresentation<XEInterface.RegisterResponse>(
                resource.getResponseEntity(), XEInterface.RegisterResponse.class).getObject();
        if (response == null)
            throw new SectioningException("Failed to check student registration status.");
        else if (!response.validStudent) {
            String reason = null;
            if (response.failureReasons != null)
                for (String m : response.failureReasons) {
                    if (reason == null)
                        reason = m;
                    else
                        reason += "\n" + m;
                }
            if (reason != null)
                throw new SectioningException(reason);
            throw new SectioningException("Failed to check student registration status.");
        }
        return response;
    }

    protected XEInterface.RegisterResponse postChanges(ClientResource resource, XEInterface.RegisterRequest req)
            throws IOException {
        if (req.isEmpty())
            req.empty();
        try {
            resource.post(new GsonRepresentation<XEInterface.RegisterRequest>(req));
        } catch (ResourceException e) {
            handleError(resource, e);
        }

        XEInterface.RegisterResponse response = new GsonRepresentation<XEInterface.RegisterResponse>(
                resource.getResponseEntity(), XEInterface.RegisterResponse.class).getObject();

        if (response == null)
            throw new SectioningException("Failed to enroll student.");
        else if (!response.validStudent) {
            String reason = null;
            if (response.failureReasons != null)
                for (String m : response.failureReasons) {
                    if (reason == null)
                        reason = m;
                    else
                        reason += "\n" + m;
                }
            if (reason != null)
                throw new SectioningException(reason);
            throw new SectioningException("Failed to enroll student.");
        }

        return response;
    }

    protected void handleError(ClientResource resource, Exception exception) {
        try {
            XEInterface.ErrorResponse response = new GsonRepresentation<XEInterface.ErrorResponse>(
                    resource.getResponseEntity(), XEInterface.ErrorResponse.class).getObject();
            XEInterface.Error error = response.getError();
            if (error != null && error.message != null) {
                throw new SectioningException(error.message);
            } else if (error != null && error.description != null) {
                throw new SectioningException(error.description);
            } else if (error != null && error.errorMessage != null) {
                throw new SectioningException(error.errorMessage);
            } else {
                throw exception;
            }
        } catch (SectioningException e) {
            throw e;
        } catch (Throwable t) {
            throw new SectioningException(exception.getMessage(), exception);
        }
    }

    protected String getBannerId(Student student) {
        String id = student.getExternalId();
        while (id.length() < 9)
            id = "0" + id;
        return id;
    }

    public String getCourseNameForCrn(Student student, String crn) {
        for (Request request : student.getRequests()) {
            Enrollment enrollment = getAssignment().getValue(request);
            if (enrollment != null && enrollment.isCourseRequest()) {
                CourseOffering course = iCourses.get(enrollment.getCourse().getId());
                for (Section section : enrollment.getSections()) {
                    Class_ clazz = iClasses.get(section.getId());
                    if (clazz != null && course != null && crn.equals(clazz.getExternalId(course)))
                        return course.getCourseName();
                }
            }
        }
        return null;
    }

    public OfferingConsentType getConsent(Student student, String crn) {
        for (Request request : student.getRequests()) {
            Enrollment enrollment = getAssignment().getValue(request);
            if (enrollment != null && enrollment.isCourseRequest()) {
                CourseOffering course = iCourses.get(enrollment.getCourse().getId());
                for (Section section : enrollment.getSections()) {
                    Class_ clazz = iClasses.get(section.getId());
                    if (clazz != null && course != null && crn.equals(clazz.getExternalId(course)))
                        return course.getConsentType();
                }
            }
        }
        return null;
    }

    protected Entity getUser() {
        return Entity.newBuilder().setExternalId(iOwnerId).setType(Entity.EntityType.MANAGER).build();
    }

    protected void add(XEInterface.RegisterRequest req, String id, boolean changeStatus) {
        if (iActionAdd == null)
            req.add(id, changeStatus);
        else if (changeStatus) {
            if (req.actionsAndOptions == null)
                req.actionsAndOptions = new ArrayList<XEInterface.RegisterAction>();
            req.actionsAndOptions.add(new XEInterface.RegisterAction(iActionAdd, id));
        } else {
            if (req.courseReferenceNumbers == null)
                req.courseReferenceNumbers = new ArrayList<XEInterface.CourseReferenceNumber>();
            req.courseReferenceNumbers.add(new XEInterface.CourseReferenceNumber(id, iActionAdd));
        }
    }

    protected void keep(XEInterface.RegisterRequest req, String id) {
        req.keep(id);
    }

    protected void drop(XEInterface.RegisterRequest req, String id) {
        if (iActionDrop == null)
            req.drop(id, null);
        else {
            if (req.actionsAndOptions == null)
                req.actionsAndOptions = new ArrayList<XEInterface.RegisterAction>();
            req.actionsAndOptions.add(new XEInterface.RegisterAction(iActionDrop, id));
        }
    }

    protected boolean removeAction(XEInterface.RegisterRequest req, String id) {
        if (req.actionsAndOptions == null)
            return false;
        for (Iterator<XEInterface.RegisterAction> i = req.actionsAndOptions.iterator(); i.hasNext();) {
            XEInterface.RegisterAction action = i.next();
            if (id.equals(action.courseReferenceNumber)) {
                i.remove();
                return true;
            }
        }
        return false;
    }

    protected static String defaultOverrides[] = new String[] { "CAMP", "CAMPUS", "CLAS", "CLASS", "CLOS", "CLOSED",
            "COLL", "COLLEGE", "CORQ", "CO-REQ", "DEGR", "DEGREE", "DEPT", "DPT-PERMIT", "DUPL", "DUP-CRSE", "LEVL",
            "LEVEL", "MAJR", "MAJOR", "PREQ", "PRE-REQ", "PROG", "PROGRAM", "TIME", "TIME-CNFLT", "CHRT",
            "COHORT", };

    protected String getDefaultOverride(Student student, String crn, String messageType) {
        String override = null;
        if ("DEPT".equals(messageType) || "SAPR".equals(messageType)) {
            OfferingConsentType consent = getConsent(student, crn);
            if (consent != null && "IN".equals(consent.getReference())) {
                override = "INST-PERMT";
            } else if (consent != null && "DP".equals(consent.getReference())) {
                override = "DPT-PERMIT";
            } else {
                override = "HONORS";
            }
        } else {
            for (int i = 0; i < defaultOverrides.length; i += 2) {
                if (messageType.equals(defaultOverrides[i]))
                    override = defaultOverrides[i + 1];
            }
        }
        return getSolver().getProperties().getProperty("Save.XE.Override." + messageType, override);
    }

    protected void checkTermination() {
        if (getTerminationCondition() != null && !getTerminationCondition().canContinue(getSolution()))
            throw new RuntimeException("The save was interrupted.");
    }

    protected void setPhase(String phase, long progressMax) {
        checkTermination();
        iProgress.setPhase(phase, progressMax);
    }

    protected void incProgress() {
        checkTermination();
        iProgress.incProgress();
    }

    protected class Worker extends Thread {
        private Iterator<Student> iStudents;

        public Worker(int index, Iterator<Student> students) {
            setName("XESaver-" + (1 + index));
            iStudents = students;
        }

        @Override
        public void run() {
            try {
                iProgress.debug(getName() + " has started.");
                while (true) {
                    Student student = null;
                    synchronized (iStudents) {
                        if (!iCanContinue) {
                            iProgress.debug(getName() + " has stopped.");
                            return;
                        }
                        if (!iStudents.hasNext())
                            break;
                        student = iStudents.next();
                        iProgress.incProgress();
                    }
                    if (!student.isDummy())
                        saveStudent(student);
                }
                iProgress.debug(getName() + " has finished.");
            } finally {
                _RootDAO.closeCurrentThreadSessions();
            }
        }
    }
}