org.unitime.timetable.onlinesectioning.OnlineSectioningServerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.unitime.timetable.onlinesectioning.OnlineSectioningServerImpl.java

Source

/*
 * UniTime 3.2 (University Timetabling Application)
 * Copyright (C) 2010, UniTime LLC, and individual contributors
 * as indicated by the @authors tag.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
*/
package org.unitime.timetable.onlinesectioning;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import net.sf.cpsolver.coursett.Constants;
import net.sf.cpsolver.coursett.model.Placement;
import net.sf.cpsolver.coursett.model.TimeLocation;
import net.sf.cpsolver.ifs.util.DataProperties;
import net.sf.cpsolver.ifs.util.DistanceMetric;
import net.sf.cpsolver.ifs.util.JProf;
import net.sf.cpsolver.ifs.util.ToolBox;
import net.sf.cpsolver.studentsct.constraint.LinkedSections;
import net.sf.cpsolver.studentsct.extension.DistanceConflict;
import net.sf.cpsolver.studentsct.extension.TimeOverlapsCounter;
import net.sf.cpsolver.studentsct.model.Config;
import net.sf.cpsolver.studentsct.model.Course;
import net.sf.cpsolver.studentsct.model.CourseRequest;
import net.sf.cpsolver.studentsct.model.Enrollment;
import net.sf.cpsolver.studentsct.model.FreeTimeRequest;
import net.sf.cpsolver.studentsct.model.Offering;
import net.sf.cpsolver.studentsct.model.Request;
import net.sf.cpsolver.studentsct.model.Section;
import net.sf.cpsolver.studentsct.model.Student;
import net.sf.cpsolver.studentsct.model.Subpart;
import net.sf.cpsolver.studentsct.reservation.Reservation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CacheMode;
import org.unitime.localization.impl.Localization;
import org.unitime.timetable.ApplicationProperties;
import org.unitime.timetable.gwt.resources.StudentSectioningMessages;
import org.unitime.timetable.gwt.shared.CourseRequestInterface;
import org.unitime.timetable.gwt.shared.SectioningException;
import org.unitime.timetable.model.Session;
import org.unitime.timetable.model.SolverParameter;
import org.unitime.timetable.model.SolverParameterDef;
import org.unitime.timetable.model.SolverParameterGroup;
import org.unitime.timetable.model.SolverPredefinedSetting;
import org.unitime.timetable.model.StudentClassEnrollment;
import org.unitime.timetable.model.TravelTime;
import org.unitime.timetable.model.dao.SessionDAO;
import org.unitime.timetable.model.dao._RootDAO;
import org.unitime.timetable.onlinesectioning.solver.StudentSchedulingAssistantWeights;
import org.unitime.timetable.onlinesectioning.updates.CheckAllOfferingsAction;
import org.unitime.timetable.onlinesectioning.updates.ReloadAllData;
import org.unitime.timetable.onlinesectioning.updates.StudentEmail;

/**
 * @author Tomas Muller
 */
public class OnlineSectioningServerImpl implements OnlineSectioningServer {
    private static StudentSectioningMessages MSG = Localization.create(StudentSectioningMessages.class);
    private Log iLog = LogFactory.getLog(OnlineSectioningServerImpl.class);
    private AcademicSessionInfo iAcademicSession = null;
    private Hashtable<Long, CourseInfo> iCourseForId = new Hashtable<Long, CourseInfo>();
    private Hashtable<String, TreeSet<CourseInfo>> iCourseForName = new Hashtable<String, TreeSet<CourseInfo>>();
    private TreeSet<CourseInfo> iCourses = new TreeSet<CourseInfo>();
    private DistanceMetric iDistanceMetric = null;
    private DataProperties iConfig = null;

    private Hashtable<Long, Course> iCourseTable = new Hashtable<Long, Course>();
    private Hashtable<Long, Section> iClassTable = new Hashtable<Long, Section>();
    private Hashtable<Long, Student> iStudentTable = new Hashtable<Long, Student>();
    private Hashtable<Long, Offering> iOfferingTable = new Hashtable<Long, Offering>();
    private Hashtable<Long, List<LinkedSections>> iLinkedSections = new Hashtable<Long, List<LinkedSections>>();

    private ReentrantReadWriteLock iLock = new ReentrantReadWriteLock();
    private MultiLock iMultiLock;
    private Map<Long, Lock> iOfferingLocks = new Hashtable<Long, Lock>();
    private AsyncExecutor iExecutor;
    private Queue<Runnable> iExecutorQueue = new LinkedList<Runnable>();
    private HashSet<CacheElement<Long>> iOfferingsToPersistExpectedSpaces = new HashSet<CacheElement<Long>>();

    OnlineSectioningServerImpl(Long sessionId, boolean waitTillStarted) throws SectioningException {
        iConfig = new ServerConfig();
        org.hibernate.Session hibSession = SessionDAO.getInstance().createNewSession();
        try {
            Session session = SessionDAO.getInstance().get(sessionId, hibSession);
            if (session == null)
                throw new SectioningException(
                        MSG.exceptionSessionDoesNotExist(sessionId == null ? "null" : sessionId.toString()));
            iAcademicSession = new AcademicSessionInfo(session);
            iLog = LogFactory.getLog(OnlineSectioningServerImpl.class.getName() + ".server["
                    + iAcademicSession.toCompactString() + "]");
            iMultiLock = new MultiLock(iAcademicSession);
            iExecutor = new AsyncExecutor();
            iExecutor.start();
            final OnlineSectioningLog.Entity user = OnlineSectioningLog.Entity.newBuilder()
                    .setExternalId(StudentClassEnrollment.SystemChange.SYSTEM.name())
                    .setName(StudentClassEnrollment.SystemChange.SYSTEM.getName())
                    .setType(OnlineSectioningLog.Entity.EntityType.OTHER).build();
            if (waitTillStarted) {
                try {
                    execute(new ReloadAllData(), user);
                } catch (Throwable exception) {
                    iLog.error("Failed to load server: " + exception.getMessage(), exception);
                    throw exception;
                }
                if (iAcademicSession.isSectioningEnabled()) {
                    try {
                        execute(new CheckAllOfferingsAction(), user);
                    } catch (Throwable exception) {
                        iLog.error("Failed to check all offerings: " + exception.getMessage(), exception);
                        throw exception;
                    }
                }
            } else {
                execute(new ReloadAllData(), user, new ServerCallback<Boolean>() {
                    @Override
                    public void onSuccess(Boolean result) {
                        if (iAcademicSession.isSectioningEnabled())
                            execute(new CheckAllOfferingsAction(), user, new ServerCallback<Boolean>() {
                                @Override
                                public void onSuccess(Boolean result) {
                                }

                                @Override
                                public void onFailure(Throwable exception) {
                                    iLog.error("Failed to check all offerings: " + exception.getMessage(),
                                            exception);
                                }
                            });
                    }

                    @Override
                    public void onFailure(Throwable exception) {
                        iLog.error("Failed to load server: " + exception.getMessage(), exception);
                    }
                });
            }
        } catch (Throwable t) {
            if (t instanceof SectioningException)
                throw (SectioningException) t;
            throw new SectioningException(MSG.exceptionUnknown(t.getMessage()), t);
        } finally {
            hibSession.close();
        }
        iDistanceMetric = new DistanceMetric(iConfig);
        TravelTime.populateTravelTimes(iDistanceMetric, sessionId);
        iLog.info("Config: " + ToolBox.dict2string(iConfig, 2));
    }

    @Override
    public DistanceMetric getDistanceMetric() {
        return iDistanceMetric;
    }

    @Override
    public AcademicSessionInfo getAcademicSession() {
        return iAcademicSession;
    }

    @Override
    public CourseInfo getCourseInfo(String course) {
        iLock.readLock().lock();
        try {
            if (course.indexOf('-') >= 0) {
                String courseName = course.substring(0, course.indexOf('-')).trim();
                String title = course.substring(course.indexOf('-') + 1).trim();
                TreeSet<CourseInfo> infos = iCourseForName.get(courseName.toLowerCase());
                if (infos != null && !infos.isEmpty())
                    for (CourseInfo info : infos)
                        if (title.equalsIgnoreCase(info.getTitle()))
                            return info;
                return null;
            } else {
                TreeSet<CourseInfo> infos = iCourseForName.get(course.toLowerCase());
                if (infos != null && !infos.isEmpty())
                    return infos.first();
                return null;
            }
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public CourseInfo getCourseInfo(Long courseId) {
        iLock.readLock().lock();
        try {
            return iCourseForId.get(courseId);
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public Student getStudent(Long studentId) {
        iLock.readLock().lock();
        try {
            return iStudentTable.get(studentId);
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public Course getCourse(Long courseId) {
        iLock.readLock().lock();
        try {
            return iCourseTable.get(courseId);
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public int distance(Section s1, Section s2) {
        if (s1.getPlacement() == null || s2.getPlacement() == null)
            return 0;
        TimeLocation t1 = s1.getTime();
        TimeLocation t2 = s2.getTime();
        if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
            return 0;
        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
        if (getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
            if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
                int dist = Placement.getDistanceInMinutes(getDistanceMetric(), s1.getPlacement(),
                        s2.getPlacement());
                if (dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()))
                    return dist;
            }
        } else {
            if (a1 + t1.getNrSlotsPerMeeting() == a2)
                return Placement.getDistanceInMinutes(getDistanceMetric(), s1.getPlacement(), s2.getPlacement());
        }
        /*
        else if (a2+t2.getNrSlotsPerMeeting()==a1) {
           return Placement.getDistance(s1.getPlacement(), s2.getPlacement());
        }
        */
        return 0;
    }

    @Override
    public Collection<CourseInfo> findCourses(String query, Integer limit) {
        iLock.readLock().lock();
        try {
            List<CourseInfo> ret = new ArrayList<CourseInfo>(limit == null ? 100 : limit);
            String queryInLowerCase = query.toLowerCase();
            for (CourseInfo c : iCourses) {
                if (c.matchCourseName(queryInLowerCase))
                    ret.add(c);
                if (limit != null && ret.size() == limit)
                    return ret;
            }
            if (queryInLowerCase.length() > 2) {
                for (CourseInfo c : iCourses) {
                    if (c.matchTitle(queryInLowerCase))
                        ret.add(c);
                    if (limit != null && ret.size() == limit)
                        return ret;
                }
            }
            return ret;
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public Collection<CourseInfo> findCourses(CourseInfoMatcher matcher) {
        iLock.readLock().lock();
        try {
            List<CourseInfo> ret = new ArrayList<CourseInfo>();
            for (CourseInfo c : iCourses) {
                if (matcher.match(c))
                    ret.add(c);
            }
            return ret;
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public URL getSectionUrl(Long courseId, Section section) {
        if (OnlineSectioningService.sSectionUrlProvider == null)
            return null;
        return OnlineSectioningService.sSectionUrlProvider.getSectionUrl(getAcademicSession(), courseId, section);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Section> getSections(CourseInfo courseInfo) {
        iLock.readLock().lock();
        try {
            ArrayList<Section> sections = new ArrayList<Section>();
            Course course = iCourseTable.get(courseInfo.getUniqueId());
            if (course == null)
                return sections;
            for (Iterator<Config> e = course.getOffering().getConfigs().iterator(); e.hasNext();) {
                Config cfg = e.next();
                for (Iterator<Subpart> f = cfg.getSubparts().iterator(); f.hasNext();) {
                    Subpart subpart = f.next();
                    for (Iterator<Section> g = subpart.getSections().iterator(); g.hasNext();) {
                        Section section = g.next();
                        sections.add(section);
                    }
                }
            }
            return sections;
        } finally {
            iLock.readLock().unlock();
        }
    }

    public static class EnrollmentSectionComparator implements Comparator<Section> {
        public boolean isParent(Section s1, Section s2) {
            Section p1 = s1.getParent();
            if (p1 == null)
                return false;
            if (p1.equals(s2))
                return true;
            return isParent(p1, s2);
        }

        public int compare(Section a, Section b) {
            if (isParent(a, b))
                return 1;
            if (isParent(b, a))
                return -1;

            int cmp = a.getSubpart().getInstructionalType()
                    .compareToIgnoreCase(b.getSubpart().getInstructionalType());
            if (cmp != 0)
                return cmp;

            return Double.compare(a.getId(), b.getId());
        }
    }

    @Override
    public Section getSection(Long classId) {
        iLock.readLock().lock();
        try {
            return iClassTable.get(classId);
        } finally {
            iLock.readLock().unlock();
        }
    }

    public static class DummyReservation extends Reservation {
        private int iPriority;
        private boolean iOver;
        private int iLimit;
        private boolean iApply;
        private boolean iMustUse;
        private boolean iAllowOverlap;

        public DummyReservation(long id, Offering offering, int priority, boolean over, int limit, boolean apply,
                boolean mustUse, boolean allowOverlap, boolean expired) {
            super(id, offering);
            iPriority = priority;
            iOver = over;
            iLimit = limit;
            iApply = apply;
            iMustUse = mustUse;
            iAllowOverlap = allowOverlap;
            setExpired(expired);
        }

        @Override
        public boolean canAssignOverLimit() {
            return iOver;
        }

        @Override
        public boolean mustBeUsed() {
            return iMustUse;
        }

        @Override
        public double getReservationLimit() {
            return iLimit;
        }

        @Override
        public int getPriority() {
            return iPriority;
        }

        @Override
        public boolean isApplicable(Student student) {
            return iApply;
        }

        @Override
        public boolean isAllowOverlap() {
            return iAllowOverlap;
        }
    }

    @Override
    public void remove(Student student) {
        iLock.writeLock().lock();
        try {
            Student s = iStudentTable.get(student.getId());
            if (s != null) {
                for (Request r : s.getRequests()) {
                    for (Request request : student.getRequests()) {
                        if (request instanceof CourseRequest) {
                            for (Course course : ((CourseRequest) request).getCourses())
                                course.getRequests().remove(request);
                        }
                        if (r.getAssignment() != null)
                            r.unassign(0);
                    }
                }
                iStudentTable.remove(student.getId());
            }
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void update(Student student) {
        iLock.writeLock().lock();
        try {
            iStudentTable.put(student.getId(), student);
            for (Request r : student.getRequests()) {
                if (r.getInitialAssignment() == null) {
                    if (r.getAssignment() != null)
                        r.unassign(0);
                } else {
                    if (r.getAssignment() == null || !r.getAssignment().equals(r.getInitialAssignment()))
                        r.assign(0, r.getInitialAssignment());
                }
            }

        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void remove(Offering offering) {
        iLock.writeLock().lock();
        try {
            for (Course course : offering.getCourses()) {
                CourseInfo ci = iCourseForId.get(course.getId());
                if (ci != null) {
                    TreeSet<CourseInfo> courses = iCourseForName.get(ci.toString());
                    if (courses != null) {
                        courses.remove(ci);
                        if (courses.isEmpty()) {
                            iCourseForName.remove(ci.toString());
                        } else if (courses.size() == 1) {
                            for (CourseInfo x : courses)
                                x.setHasUniqueName(true);
                        }
                    }
                    iCourseForId.remove(ci.getUniqueId());
                    iCourses.remove(ci);
                }
                iCourseTable.remove(course.getId());
            }
            iOfferingTable.remove(offering.getId());
            for (Config config : offering.getConfigs()) {
                for (Subpart subpart : config.getSubparts())
                    for (Section section : subpart.getSections())
                        iClassTable.remove(section.getId());
                for (Enrollment enrollment : new ArrayList<Enrollment>(config.getEnrollments()))
                    enrollment.variable().unassign(0);
            }
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void update(CourseInfo info) {
        iLock.writeLock().lock();
        try {
            CourseInfo old = iCourseForId.get(info.getUniqueId());
            iCourseForId.put(info.getUniqueId(), info);
            TreeSet<CourseInfo> courses = iCourseForName.get(info.toString());
            if (courses == null) {
                courses = new TreeSet<CourseInfo>();
                iCourseForName.put(info.toString(), courses);
            }
            if (old != null) {
                courses.remove(old);
                iCourses.remove(old);
            }
            courses.add(info);
            iCourses.add(info);
            if (courses.size() == 1)
                for (CourseInfo x : courses)
                    x.setHasUniqueName(true);
            else if (courses.size() > 1)
                for (CourseInfo x : courses)
                    x.setHasUniqueName(false);
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void update(Offering offering) {
        iLock.writeLock().lock();
        try {
            Offering old = iOfferingTable.get(offering.getId());
            if (old != null)
                remove(old);
            for (Course course : offering.getCourses())
                iCourseTable.put(course.getId(), course);
            iOfferingTable.put(offering.getId(), offering);
            for (Config config : offering.getConfigs())
                for (Subpart subpart : config.getSubparts())
                    for (Section section : subpart.getSections())
                        iClassTable.put(section.getId(), section);
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public Offering getOffering(Long offeringId) {
        iLock.readLock().lock();
        try {
            return iOfferingTable.get(offeringId);
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public void clearAll() {
        iLock.writeLock().lock();
        try {
            iClassTable.clear();
            iStudentTable.clear();
            iOfferingTable.clear();
            iCourseTable.clear();
            iCourseForId.clear();
            iCourseForName.clear();
            iCourses.clear();
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void clearAllStudents() {
        iLock.writeLock().lock();
        try {
            for (Student student : iStudentTable.values()) {
                for (Iterator<Request> e = student.getRequests().iterator(); e.hasNext();) {
                    Request r = (Request) e.next();
                    if (r.getAssignment() != null)
                        r.unassign(0);
                }
            }
            iStudentTable.clear();
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public <E> E execute(OnlineSectioningAction<E> action, OnlineSectioningLog.Entity user)
            throws SectioningException {
        long c0 = OnlineSectioningHelper.getCpuTime();
        String cacheMode = getConfig().getProperty(action.name() + ".CacheMode",
                getConfig().getProperty("CacheMode"));
        OnlineSectioningHelper h = new OnlineSectioningHelper(user,
                cacheMode == null ? null : CacheMode.parse(cacheMode));
        try {
            h.addMessageHandler(new OnlineSectioningHelper.DefaultMessageLogger(
                    LogFactory.getLog(OnlineSectioningServer.class.getName() + "." + action.name() + "["
                            + getAcademicSession().toCompactString() + "]")));
            h.addAction(action, getAcademicSession());
            E ret = action.execute(this, h);
            if (h.getAction() != null) {
                if (ret == null)
                    h.getAction().setResult(OnlineSectioningLog.Action.ResultType.NULL);
                else if (ret instanceof Boolean)
                    h.getAction().setResult((Boolean) ret ? OnlineSectioningLog.Action.ResultType.TRUE
                            : OnlineSectioningLog.Action.ResultType.FALSE);
                else
                    h.getAction().setResult(OnlineSectioningLog.Action.ResultType.SUCCESS);
            }
            return ret;
        } catch (Exception e) {
            h.error("Execution failed: " + e.getMessage(), e);
            if (h.getAction() != null) {
                h.getAction().setResult(OnlineSectioningLog.Action.ResultType.FAILURE);
                if (e.getCause() != null && e instanceof SectioningException)
                    h.getAction()
                            .addMessage(OnlineSectioningLog.Message.newBuilder()
                                    .setLevel(OnlineSectioningLog.Message.Level.FATAL)
                                    .setText(e.getCause().getClass().getName() + ": " + e.getCause().getMessage()));
                else
                    h.getAction()
                            .addMessage(OnlineSectioningLog.Message.newBuilder()
                                    .setLevel(OnlineSectioningLog.Message.Level.FATAL)
                                    .setText(e.getMessage() == null ? "null" : e.getMessage()));
            }
            if (e instanceof SectioningException)
                throw (SectioningException) e;
            throw new SectioningException(MSG.exceptionUnknown(e.getMessage()), e);
        } finally {
            if (h.getAction() != null)
                h.getAction().setEndTime(System.currentTimeMillis())
                        .setCpuTime(OnlineSectioningHelper.getCpuTime() - c0);
            iLog.debug("Executed: " + h.getLog() + " (" + h.getLog().toByteArray().length + " bytes)");
            OnlineSectioningLogger.getInstance().record(h.getLog());
        }
    }

    @Override
    public Lock readLock() {
        iLock.readLock().lock();
        return new Lock() {
            public void release() {
                iLock.readLock().unlock();
            }
        };
    }

    @Override
    public Lock writeLock() {
        iLock.writeLock().lock();
        return new Lock() {
            public void release() {
                iLock.writeLock().unlock();
            }
        };
    }

    @Override
    public Lock lockAll() {
        iLock.writeLock().lock();
        return new Lock() {
            public void release() {
                iLock.writeLock().unlock();
            }
        };
    }

    @Override
    public Lock lockStudent(Long studentId, Collection<Long> offeringIds, boolean excludeLockedOfferings) {
        Set<Long> ids = new HashSet<Long>();
        iLock.readLock().lock();
        try {
            ids.add(-studentId);
            if (offeringIds != null)
                for (Long offeringId : offeringIds)
                    if (!excludeLockedOfferings || !iOfferingLocks.containsKey(offeringId))
                        ids.add(offeringId);

            Student student = iStudentTable.get(studentId);

            if (student != null)
                for (Request r : student.getRequests()) {
                    Offering o = (r.getAssignment() == null ? null : r.getAssignment().getOffering());
                    if (o != null && (!excludeLockedOfferings || !iOfferingLocks.containsKey(o.getId())))
                        ids.add(o.getId());
                }
        } finally {
            iLock.readLock().unlock();
        }
        return iMultiLock.lock(ids);
    }

    @Override
    public Lock lockOffering(Long offeringId, Collection<Long> studentIds, boolean excludeLockedOffering) {
        Set<Long> ids = new HashSet<Long>();
        iLock.readLock().lock();
        try {
            if (!excludeLockedOffering || !iOfferingLocks.containsKey(offeringId))
                ids.add(offeringId);

            if (studentIds != null)
                for (Long studentId : studentIds)
                    ids.add(-studentId);

            Offering offering = iOfferingTable.get(offeringId);

            if (offering != null)
                for (Course course : offering.getCourses())
                    for (CourseRequest request : course.getRequests())
                        ids.add(-request.getStudent().getId());
        } finally {
            iLock.readLock().unlock();
        }
        return iMultiLock.lock(ids);
    }

    @Override
    public Lock lockClass(Long classId, Collection<Long> studentIds) {
        Set<Long> ids = new HashSet<Long>();
        iLock.readLock().lock();
        try {
            if (studentIds != null)
                for (Long studentId : studentIds)
                    ids.add(-studentId);

            Section section = iClassTable.get(classId);
            if (section != null) {
                for (Enrollment enrollment : section.getEnrollments())
                    ids.add(-enrollment.getStudent().getId());
                ids.add(section.getSubpart().getConfig().getOffering().getId());
            }
        } finally {
            iLock.readLock().unlock();
        }
        return iMultiLock.lock(ids);
    }

    private Long getOfferingIdFromCourseName(String courseName) {
        if (courseName == null)
            return null;
        CourseInfo c = getCourseInfo(courseName);
        if (c == null)
            return null;
        Course course = iCourseTable.get(c.getUniqueId());
        return (course == null ? null : course.getOffering().getId());
    }

    public Lock lockRequest(CourseRequestInterface request) {
        Set<Long> ids = new HashSet<Long>();
        iLock.readLock().lock();
        try {
            if (request.getStudentId() != null)
                ids.add(-request.getStudentId());
            for (CourseRequestInterface.Request r : request.getCourses()) {
                if (r.hasRequestedCourse()) {
                    Long id = getOfferingIdFromCourseName(r.getRequestedCourse());
                    if (id != null)
                        ids.add(id);
                }
                if (r.hasFirstAlternative()) {
                    Long id = getOfferingIdFromCourseName(r.getFirstAlternative());
                    if (id != null)
                        ids.add(id);
                }
                if (r.hasSecondAlternative()) {
                    Long id = getOfferingIdFromCourseName(r.getSecondAlternative());
                    if (id != null)
                        ids.add(id);
                }
            }
            for (CourseRequestInterface.Request r : request.getAlternatives()) {
                if (r.hasRequestedCourse()) {
                    Long id = getOfferingIdFromCourseName(r.getRequestedCourse());
                    if (id != null)
                        ids.add(id);
                }
                if (r.hasFirstAlternative()) {
                    Long id = getOfferingIdFromCourseName(r.getFirstAlternative());
                    if (id != null)
                        ids.add(id);
                }
                if (r.hasSecondAlternative()) {
                    Long id = getOfferingIdFromCourseName(r.getSecondAlternative());
                    if (id != null)
                        ids.add(id);
                }
            }
        } finally {
            iLock.readLock().unlock();
        }
        return iMultiLock.lock(ids);
    }

    public void notifyStudentChanged(Long studentId, List<Request> oldRequests, List<Request> newRequests,
            OnlineSectioningLog.Entity user) {
        Student student = getStudent(studentId);
        if (student != null) {
            String message = "Student " + student.getId() + " changed.";
            if (oldRequests != null) {
                message += "\n  Previous schedule:";
                for (Request r : oldRequests) {
                    message += "\n    " + r.getName()
                            + (r instanceof FreeTimeRequest || r.getInitialAssignment() != null ? ""
                                    : " NOT ASSIGNED");
                    if (r instanceof CourseRequest && r.getInitialAssignment() != null) {
                        for (Section s : r.getInitialAssignment().getSections()) {
                            message += "\n      " + s.getSubpart().getName() + " "
                                    + s.getName(r.getInitialAssignment().getCourse().getId())
                                    + (s.getTime() == null ? "" : " " + s.getTime().getLongName())
                                    + (s.getNrRooms() == 0 ? "" : " " + s.getPlacement().getRoomName(", "));
                        }
                    }
                }
            }
            if (newRequests != null) {
                message += "\n  New schedule:";
                for (Request r : newRequests) {
                    message += "\n    " + r.getName()
                            + (r instanceof FreeTimeRequest || r.getInitialAssignment() != null ? ""
                                    : " NOT ASSIGNED");
                    if (r instanceof CourseRequest && r.getInitialAssignment() != null) {
                        for (Section s : r.getInitialAssignment().getSections()) {
                            message += "\n      " + s.getSubpart().getName() + " "
                                    + s.getName(r.getInitialAssignment().getCourse().getId())
                                    + (s.getTime() == null ? "" : " " + s.getTime().getLongName())
                                    + (s.getNrRooms() == 0 ? "" : " " + s.getPlacement().getRoomName(", "));
                        }
                    }
                }
            }
            iLog.info(message);
            if (getAcademicSession().isSectioningEnabled()
                    && "true".equals(ApplicationProperties.getProperty("unitime.enrollment.email", "true"))) {
                execute(new StudentEmail(studentId, oldRequests, newRequests), user, new ServerCallback<Boolean>() {
                    @Override
                    public void onFailure(Throwable exception) {
                        iLog.error("Failed to notify student: " + exception.getMessage(), exception);
                    }

                    @Override
                    public void onSuccess(Boolean result) {
                    }
                });
            }
        }
    }

    @Override
    public void notifyStudentChanged(Long studentId, Request request, Enrollment oldEnrollment,
            OnlineSectioningLog.Entity user) {
        Student student = getStudent(studentId);
        if (student != null) {
            String message = "Student " + student.getId() + " changed.";
            if (oldEnrollment != null) {
                message += "\n  Previous assignment:";
                message += "\n    " + request.getName()
                        + (request instanceof FreeTimeRequest || oldEnrollment != null ? "" : " NOT ASSIGNED");
                if (request instanceof CourseRequest && oldEnrollment != null) {
                    for (Section s : oldEnrollment.getSections()) {
                        message += "\n      " + s.getSubpart().getName() + " "
                                + s.getName(oldEnrollment.getCourse().getId())
                                + (s.getTime() == null ? "" : " " + s.getTime().getLongName())
                                + (s.getNrRooms() == 0 ? "" : " " + s.getPlacement().getRoomName(", "));
                    }
                }
            }
            message += "\n  New schedule:";
            message += "\n    " + request.getName()
                    + (request instanceof FreeTimeRequest || request.getInitialAssignment() != null ? ""
                            : " NOT ASSIGNED");
            if (request instanceof CourseRequest && request.getInitialAssignment() != null) {
                for (Section s : request.getInitialAssignment().getSections()) {
                    message += "\n      " + s.getSubpart().getName() + " "
                            + s.getName(request.getInitialAssignment().getCourse().getId())
                            + (s.getTime() == null ? "" : " " + s.getTime().getLongName())
                            + (s.getNrRooms() == 0 ? "" : " " + s.getPlacement().getRoomName(", "));
                }
            }
            iLog.info(message);
            if (getAcademicSession().isSectioningEnabled()
                    && "true".equals(ApplicationProperties.getProperty("unitime.enrollment.email", "true"))) {
                if (oldEnrollment == null) {
                    oldEnrollment = new Enrollment(request, 0,
                            (request instanceof CourseRequest ? ((CourseRequest) request).getCourses().get(0)
                                    : null),
                            null, null, null);
                }
                execute(new StudentEmail(studentId, oldEnrollment, student.getRequests()), user,
                        new ServerCallback<Boolean>() {
                            @Override
                            public void onFailure(Throwable exception) {
                                iLog.error("Failed to notify student: " + exception.getMessage(), exception);
                            }

                            @Override
                            public void onSuccess(Boolean result) {
                            }
                        });
            }
        }
    }

    @Override
    public boolean isOfferingLocked(Long offeringId) {
        synchronized (iOfferingLocks) {
            return iOfferingLocks.containsKey(offeringId);
        }
    }

    @Override
    public void lockOffering(Long offeringId) {
        synchronized (iOfferingLocks) {
            if (iOfferingLocks.containsKey(offeringId))
                return;
        }
        Lock lock = iMultiLock.lock(offeringId);
        synchronized (iOfferingLocks) {
            if (iOfferingLocks.containsKey(offeringId))
                lock.release();
            else
                iOfferingLocks.put(offeringId, lock);
        }
    }

    @Override
    public void unlockOffering(Long offeringId) {
        synchronized (iOfferingLocks) {
            Lock lock = iOfferingLocks.remove(offeringId);
            if (lock != null)
                lock.release();
        }
    }

    @Override
    public Collection<Long> getLockedOfferings() {
        synchronized (iOfferingLocks) {
            return new ArrayList<Long>(iOfferingLocks.keySet());
        }
    }

    @Override
    public void releaseAllOfferingLocks() {
        synchronized (iOfferingLocks) {
            for (Lock lock : iOfferingLocks.values())
                lock.release();
            iOfferingLocks.clear();
        }
    }

    @Override
    public <E> void execute(final OnlineSectioningAction<E> action, final OnlineSectioningLog.Entity user,
            final ServerCallback<E> callback) throws SectioningException {
        synchronized (iExecutorQueue) {
            iExecutorQueue.offer(new Runnable() {
                @Override
                public void run() {
                    try {
                        callback.onSuccess(execute(action, user));
                    } catch (Throwable t) {
                        callback.onFailure(t);
                    }
                }

                @Override
                public String toString() {
                    return action.name();
                }
            });
            iExecutorQueue.notify();
        }
    }

    public class AsyncExecutor extends Thread {
        private boolean iStop = false;

        public AsyncExecutor() {
            setName("AsyncExecutor[" + getAcademicSession() + "]");
            setDaemon(true);
        }

        public void run() {
            Runnable job;
            while (!iStop) {
                synchronized (iExecutorQueue) {
                    job = iExecutorQueue.poll();
                    if (job == null) {
                        try {
                            iLog.info("Executor is waiting for a new job...");
                            iExecutorQueue.wait();
                        } catch (InterruptedException e) {
                        }
                        continue;
                    }
                }
                job.run();
                if (_RootDAO.closeCurrentThreadSessions())
                    iLog.warn("Job " + job + " did not close current-thread hibernate session.");
            }
            iLog.info("Executor stopped.");
        }

    }

    @Override
    public void unload() {
        if (iExecutor != null) {
            iExecutor.iStop = true;
            synchronized (iExecutorQueue) {
                iExecutorQueue.notify();
            }
        }
    }

    @Override
    public DataProperties getConfig() {
        return iConfig;
    }

    @Override
    public void addLinkedSections(LinkedSections link) {
        iLock.writeLock().lock();
        try {
            for (Offering offering : link.getOfferings()) {
                List<LinkedSections> list = iLinkedSections.get(offering.getId());
                if (list == null) {
                    list = new ArrayList<LinkedSections>();
                    iLinkedSections.put(offering.getId(), list);
                }
                list.add(link);
            }
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public Collection<LinkedSections> getLinkedSections(Long offeringId) {
        iLock.readLock().lock();
        try {
            return iLinkedSections.get(offeringId);

        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public void removeLinkedSections(Long offeringId) {
        iLock.writeLock().lock();
        try {
            List<LinkedSections> list = iLinkedSections.get(offeringId);
            if (list != null && !list.isEmpty())
                for (LinkedSections link : new ArrayList<LinkedSections>(list)) {
                    for (Offering offering : link.getOfferings()) {
                        List<LinkedSections> l = iLinkedSections.get(offering.getId());
                        if (l != null)
                            l.remove(link);
                    }
                }
        } finally {
            iLock.writeLock().unlock();
        }
    }

    @Override
    public void persistExpectedSpaces(Long offeringId) {
        synchronized (iOfferingsToPersistExpectedSpaces) {
            iOfferingsToPersistExpectedSpaces.add(new CacheElement<Long>(offeringId));
        }
    }

    @Override
    public List<Long> getOfferingsToPersistExpectedSpaces(long minimalAge) {
        List<Long> offeringIds = new ArrayList<Long>();
        long current = JProf.currentTimeMillis();
        synchronized (iOfferingsToPersistExpectedSpaces) {
            for (Iterator<CacheElement<Long>> i = iOfferingsToPersistExpectedSpaces.iterator(); i.hasNext();) {
                CacheElement<Long> c = i.next();
                if (current - c.created() >= minimalAge) {
                    offeringIds.add(c.element());
                    i.remove();
                }
            }
        }
        return offeringIds;
    }

    @Override
    public boolean needPersistExpectedSpaces(Long offeringId) {
        synchronized (iOfferingsToPersistExpectedSpaces) {
            return iOfferingsToPersistExpectedSpaces.remove(offeringId);
        }
    }

    @Override
    public Collection<Student> findStudents(StudentMatcher matcher) {
        iLock.readLock().lock();
        try {
            List<Student> ret = new ArrayList<Student>();
            for (Student s : iStudentTable.values())
                if (matcher.match(s))
                    ret.add(s);
            return ret;
        } finally {
            iLock.readLock().unlock();
        }
    }

    @Override
    public boolean checkDeadline(Section section, Deadline type) {
        if (!"true".equals(ApplicationProperties.getProperty("unitime.enrollment.deadline", "true")))
            return true;

        CourseInfo info = getCourseInfo(section.getSubpart().getConfig().getOffering().getCourses().get(0).getId());
        int deadline = 0;
        switch (type) {
        case NEW:
            if (info != null && info.getLastWeekToEnroll() != null)
                deadline = info.getLastWeekToEnroll();
            else
                deadline = getAcademicSession().getLastWeekToEnroll();
            break;
        case CHANGE:
            if (info != null && info.getLastWeekToChange() != null)
                deadline = info.getLastWeekToChange();
            else
                deadline = getAcademicSession().getLastWeekToChange();
            break;
        case DROP:
            if (info != null && info.getLastWeekToDrop() != null)
                deadline = info.getLastWeekToDrop();
            else
                deadline = getAcademicSession().getLastWeekToDrop();
            break;
        }
        long start = getAcademicSession().getSessionBeginDate().getTime();
        long now = new Date().getTime();
        int week = 0;
        if (now >= start) {
            week = (int) ((now - start) / (1000 * 60 * 60 * 24 * 7)) + 1;
        } else {
            week = -((int) (start - now) / (1000 * 60 * 60 * 24 * 7));
        }

        if (section.getTime() == null)
            return week <= deadline; // no time, just compare week and the deadline

        int offset = 0;
        long time = getAcademicSession().getDatePatternFirstDate().getTime()
                + (long) section.getTime().getWeekCode().nextSetBit(0) * (1000l * 60l * 60l * 24l);
        if (time >= start) {
            offset = (int) ((time - start) / (1000 * 60 * 60 * 24 * 7));
        } else {
            offset = -((int) (start - time) / (1000 * 60 * 60 * 24 * 7)) - 1;
        }

        return week <= deadline + offset;
    }

    private static class ServerConfig extends DataProperties {
        private static final long serialVersionUID = 1L;

        private ServerConfig() {
            super();
            setProperty("Neighbour.BranchAndBoundTimeout", "1000");
            setProperty("Suggestions.Timeout", "1000");
            setProperty("Extensions.Classes",
                    DistanceConflict.class.getName() + ";" + TimeOverlapsCounter.class.getName());
            setProperty("StudentWeights.Class", StudentSchedulingAssistantWeights.class.getName());
            setProperty("StudentWeights.PriorityWeighting", "true");
            setProperty("StudentWeights.LeftoverSpread", "true");
            setProperty("StudentWeights.BalancingFactor", "0.0");
            setProperty("StudentWeights.MultiCriteria", "true");
            setProperty("Reservation.CanAssignOverTheLimit", "true");
            setProperty("General.SaveDefaultProperties", "false");
            org.hibernate.Session hibSession = SessionDAO.getInstance().createNewSession();
            try {
                for (SolverParameterDef def : (List<SolverParameterDef>) hibSession
                        .createQuery(
                                "from SolverParameterDef x where x.group.type = :type and x.default is not null")
                        .setInteger("type", SolverParameterGroup.sTypeStudent).list()) {
                    setProperty(def.getName(), def.getDefault());
                }
                SolverPredefinedSetting settings = (SolverPredefinedSetting) hibSession
                        .createQuery("from SolverPredefinedSetting x where x.name = :reference")
                        .setString("reference", "StudentSct.Online").setMaxResults(1).uniqueResult();
                if (settings != null) {
                    for (SolverParameter param : settings.getParameters()) {
                        if (!param.getDefinition().isVisible().booleanValue())
                            continue;
                        if (param.getDefinition().getGroup().getType() != SolverParameterGroup.sTypeStudent)
                            continue;
                        setProperty(param.getDefinition().getName(), param.getValue());
                    }
                    setProperty("General.SettingsId", settings.getUniqueId().toString());
                }
                if (getProperty("Distances.Ellipsoid") == null
                        || "DEFAULT".equals(getProperty("Distances.Ellipsoid")))
                    setProperty("Distances.Ellipsoid", ApplicationProperties
                            .getProperty("unitime.distance.ellipsoid", DistanceMetric.Ellipsoid.LEGACY.name()));
                if ("Priority".equals(getProperty("StudentWeights.Mode")))
                    setProperty("StudentWeights.PriorityWeighting", "true");
                else if ("Equal".equals(getProperty("StudentWeights.Mode")))
                    setProperty("StudentWeights.PriorityWeighting", "false");
            } finally {
                hibSession.close();
            }
        }

        @Override
        public String getProperty(String key) {
            String value = ApplicationProperties.getProperty("unitime.sectioning.config." + key);
            return value == null ? super.getProperty(key) : value;
        }

        @Override
        public String getProperty(String key, String defaultValue) {
            String value = ApplicationProperties.getProperty("unitime.sectioning.config." + key);
            return value == null ? super.getProperty(key, defaultValue) : value;
        }
    }

}