edu.northwestern.bioinformatics.studycalendar.service.SubjectService.java Source code

Java tutorial

Introduction

Here is the source code for edu.northwestern.bioinformatics.studycalendar.service.SubjectService.java

Source

/*L
 * Copyright Northwestern University.
 *
 * Distributed under the OSI-approved BSD 3-Clause License.
 * See http://ncip.github.io/psc/LICENSE.txt for details.
 */

package edu.northwestern.bioinformatics.studycalendar.service;

import edu.northwestern.bioinformatics.studycalendar.StudyCalendarSystemException;
import edu.northwestern.bioinformatics.studycalendar.StudyCalendarValidationException;
import edu.northwestern.bioinformatics.studycalendar.dao.SubjectDao;
import edu.northwestern.bioinformatics.studycalendar.domain.BlackoutDate;
import edu.northwestern.bioinformatics.studycalendar.domain.Gender;
import edu.northwestern.bioinformatics.studycalendar.domain.NextStudySegmentMode;
import edu.northwestern.bioinformatics.studycalendar.domain.Period;
import edu.northwestern.bioinformatics.studycalendar.domain.PlannedActivity;
import edu.northwestern.bioinformatics.studycalendar.domain.Population;
import edu.northwestern.bioinformatics.studycalendar.domain.RelativeRecurringBlackout;
import edu.northwestern.bioinformatics.studycalendar.domain.ScheduledActivity;
import edu.northwestern.bioinformatics.studycalendar.domain.ScheduledActivityMode;
import edu.northwestern.bioinformatics.studycalendar.domain.ScheduledActivityState;
import edu.northwestern.bioinformatics.studycalendar.domain.ScheduledCalendar;
import edu.northwestern.bioinformatics.studycalendar.domain.ScheduledStudySegment;
import edu.northwestern.bioinformatics.studycalendar.domain.Site;
import edu.northwestern.bioinformatics.studycalendar.domain.SpecificDateBlackout;
import edu.northwestern.bioinformatics.studycalendar.domain.StudySegment;
import edu.northwestern.bioinformatics.studycalendar.domain.StudySite;
import edu.northwestern.bioinformatics.studycalendar.domain.StudySubjectAssignment;
import edu.northwestern.bioinformatics.studycalendar.domain.Subject;
import edu.northwestern.bioinformatics.studycalendar.domain.WeekdayBlackout;
import edu.northwestern.bioinformatics.studycalendar.domain.delta.Amendment;
import edu.northwestern.bioinformatics.studycalendar.security.authorization.PscUser;
import edu.northwestern.bioinformatics.studycalendar.service.presenter.Registration;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Rhett Sutphin
 */
// TODO: split out schedule-specific parts into ScheduleService
@Transactional
public class SubjectService {
    private final Logger log = LoggerFactory.getLogger(getClass());

    public static final String RESCHEDULED = "Rescheduled. ";

    private SubjectDao subjectDao;
    private AmendmentService amendmentService;

    public StudySubjectAssignment assignSubject(StudySite studySite, Registration registration) {
        return this.assignSubject(registration.getSubject(), studySite, registration.getFirstStudySegment(),
                registration.getDate(), registration.getDesiredStudySubjectAssignmentId(),
                registration.getStudySubjectId(), registration.getPopulations(),
                registration.getStudySubjectCalendarManager());
    }

    private StudySubjectAssignment assignSubject(Subject subject, StudySite studySite,
            StudySegment studySegmentOfFirstEpoch, Date startDate, String assignmentGridIdentifier,
            String studySubjectId, Set<Population> populations, PscUser manager) {
        Amendment currentAmendment = studySite.getCurrentApprovedAmendment();
        if (currentAmendment == null) {
            throw new StudyCalendarSystemException("The template for %s has not been approved by %s",
                    studySite.getStudy().getName(), studySite.getSite().getName());
        }

        populations = (populations == null) ? Collections.<Population>emptySet() : populations;

        StudySubjectAssignment spa = new StudySubjectAssignment();
        spa.setSubject(subject);
        spa.setStudySite(studySite);
        spa.setStartDate(startDate);
        spa.setGridId(assignmentGridIdentifier);
        spa.setStudySubjectCalendarManager(manager == null ? null : manager.getCsmUser());
        spa.setCurrentAmendment(currentAmendment);
        spa.setStudySubjectId(studySubjectId);
        spa.setPopulations(populations);
        subject.addAssignment(spa);
        scheduleStudySegment(spa, studySegmentOfFirstEpoch, startDate, NextStudySegmentMode.PER_PROTOCOL);
        subjectDao.save(subject);
        return spa;
    }

    public ScheduledStudySegment scheduleStudySegment(StudySubjectAssignment assignment, StudySegment studySegment,
            Date startDate, NextStudySegmentMode mode) {
        if (assignment.getEndDate() != null)
            return null;
        ScheduledCalendar calendar = assignment.getScheduledCalendar();
        if (calendar == null) {
            calendar = new ScheduledCalendar();
            assignment.setScheduledCalendar(calendar);
        }

        if (mode == NextStudySegmentMode.IMMEDIATE) {
            String cancellationReason = "Immediate transition to " + studySegment.getQualifiedName();
            for (ScheduledStudySegment existingStudySegment : calendar.getScheduledStudySegments()) {
                existingStudySegment.unscheduleOutstandingEvents(cancellationReason);
            }
        }

        Amendment sourceAmendment = assignment.getCurrentAmendment();
        StudySegment amendedSegment = amendmentService.getAmendedNode(studySegment, sourceAmendment);

        if (amendedSegment == null) {
            throw new StudyCalendarSystemException("Could not find a node %s in the target study", studySegment);
        }
        Integer studySegmentStartDay = studySegment.getDayRange().getStartDay();

        ScheduledStudySegment scheduledStudySegment = new ScheduledStudySegment();
        scheduledStudySegment.setStudySegment(studySegment);
        scheduledStudySegment.setStartDate(startDate);
        scheduledStudySegment.setStartDay(studySegmentStartDay);
        calendar.addStudySegment(scheduledStudySegment);

        for (Period period : amendedSegment.getPeriods()) {
            schedulePeriod(period, sourceAmendment, "Initialized from template", scheduledStudySegment);
        }

        // Sort in the same order they'll be coming out of the database (for consistency)
        Collections.sort(scheduledStudySegment.getActivities(), DatabaseEventOrderComparator.INSTANCE);

        Site site = calendar.getAssignment().getStudySite().getSite();
        avoidBlackoutDates(scheduledStudySegment, site);
        subjectDao.save(assignment.getSubject());

        return scheduledStudySegment;
    }

    /**
     * Derives Scheduled StudySegment for the preview.
     */
    public void scheduleStudySegmentPreview(ScheduledCalendar calendar, StudySegment studySegment, Date startDate) {
        log.debug("Appending {} to preview starting at {}", studySegment, startDate);
        Integer studySegmentStartDay = studySegment.getDayRange().getStartDay();

        ScheduledStudySegment scheduledStudySegment = new ScheduledStudySegment();
        scheduledStudySegment.setStudySegment(studySegment);
        scheduledStudySegment.setStartDate(startDate);
        scheduledStudySegment.setStartDay(studySegmentStartDay);
        calendar.addStudySegment(scheduledStudySegment);

        for (Period period : studySegment.getPeriods()) {
            schedulePeriod(period, null, "Initialized from template", scheduledStudySegment);
        }

        // Sort in the same order they'll be coming out of the database (for consistency)
        Collections.sort(scheduledStudySegment.getActivities(), DatabaseEventOrderComparator.INSTANCE);
    }

    /**
     * Derives scheduled events from the given period and applies them to the given scheduled studySegment.
     * <p>
     * The input period should already match the provided sourceAmendment.
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    public void schedulePeriod(Period period, Amendment sourceAmendment, String reason,
            ScheduledStudySegment targetStudySegment) {
        schedulePeriod(period, sourceAmendment, reason, null, targetStudySegment);
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    protected void schedulePeriod(Period period, Amendment sourceAmendment, String reason,
            Population restrictToPopulation, ScheduledStudySegment targetStudySegment) {
        log.debug("Adding events from period {}", period);
        for (PlannedActivity plannedActivity : period.getPlannedActivities()) {
            schedulePlannedActivity(plannedActivity, period, sourceAmendment, reason, restrictToPopulation,
                    targetStudySegment);
        }
    }

    /**
     * Derives one repetition's worth of scheduled events from the given period and applies them to
     * the given scheduled studySegment.
     * <p>
     * The input period should already match the provided sourceAmendment.
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    public void schedulePeriod(Period period, Amendment sourceAmendment, String reason,
            ScheduledStudySegment targetStudySegment, int repetitionNumber) {
        schedulePeriod(period, sourceAmendment, reason, null, targetStudySegment, repetitionNumber);
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    protected void schedulePeriod(Period period, Amendment sourceAmendment, String reason,
            Population restrictToPopulation, ScheduledStudySegment targetStudySegment, int repetitionNumber) {
        log.debug("Adding events for rep {} from period {}", repetitionNumber, period);
        for (PlannedActivity plannedActivity : period.getPlannedActivities()) {
            schedulePlannedActivity(plannedActivity, period, sourceAmendment, reason, restrictToPopulation,
                    targetStudySegment, repetitionNumber);
        }
    }

    /**
     * Derives scheduled events from the given planned event and applies them to the given scheduled studySegment.
     * <p>
     * The input plannedActivity should already match the provided sourceAmendment.
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    public void schedulePlannedActivity(PlannedActivity plannedActivity, Period period, Amendment sourceAmendment,
            String reason, ScheduledStudySegment targetStudySegment) {
        // TODO: this seems to be ignoring the PA's population.  Examine and determine whether this is a bug.  RMS20080919.
        schedulePlannedActivity(plannedActivity, period, sourceAmendment, reason, null, targetStudySegment);
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    protected void schedulePlannedActivity(PlannedActivity plannedActivity, Period period,
            Amendment sourceAmendment, String reason, Population restrictToPopulation,
            ScheduledStudySegment targetStudySegment) {
        for (int r = 0; r < period.getRepetitions(); r++) {
            schedulePlannedActivity(plannedActivity, period, sourceAmendment, reason, restrictToPopulation,
                    targetStudySegment, r);
        }
    }

    /**
     * Derives a single scheduled event from the given planned event and applies it to the given scheduled studySegment.
     * <p>
     * The input plannedActivity should already match the provided sourceAmendment.
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    protected void schedulePlannedActivity(PlannedActivity plannedActivity, Period period,
            Amendment sourceAmendment, String reason, Population restrictToPopulation,
            ScheduledStudySegment targetStudySegment, int repetitionNumber) {
        if (targetStudySegment.getScheduledCalendar().getAssignment() != null) {
            Set<Population> subjectPopulations = targetStudySegment.getScheduledCalendar().getAssignment()
                    .getPopulations();
            if (plannedActivity.getPopulation() != null
                    && !subjectPopulations.contains(plannedActivity.getPopulation())) {
                log.debug("Skipping {} since the subject is not in population {}", plannedActivity,
                        plannedActivity.getPopulation().getAbbreviation());
                return;
            }
        }
        if (restrictToPopulation != null && !restrictToPopulation.equals(plannedActivity.getPopulation())) {
            log.debug("Only adding planned activities for the {} population", restrictToPopulation);
            return;
        }

        log.debug("Adding event {} from planned activity {}", repetitionNumber, plannedActivity);

        // amount needed to shift the relative days in the period such that
        // the relative day 0 falls on studySegmentStateDate.  E.g., if the studySegment starts on
        // day 1, the normalizationFactor needs to shift everything down 1.
        // if it starts on -7, it needs to shift everything up 7.
        int normalizationFactor = targetStudySegment.getStartDay() * -1;

        int repOffset = normalizationFactor + period.getStartDay()
                + period.getDuration().getDays() * repetitionNumber;
        log.debug(" - rep {}; offset: {}", repetitionNumber, repOffset);
        ScheduledActivity event = createEmptyScheduledActivityFor(plannedActivity);
        event.setRepetitionNumber(repetitionNumber);
        event.setIdealDate(idealDate(repOffset + plannedActivity.getDay(), targetStudySegment.getStartDate()));

        ScheduledActivityState initialState = plannedActivity.getInitialScheduledMode().createStateInstance();
        initialState.setReason(reason);
        initialState.setDate(event.getIdealDate());
        event.changeState(initialState);

        event.setDetails(plannedActivity.getDetails());
        event.setActivity(plannedActivity.getActivity());
        event.setLabels(plannedActivity.getLabelsForRepetition(repetitionNumber));
        event.setSourceAmendment(sourceAmendment);

        targetStudySegment.addEvent(event);
    }

    // factored out to allow tests to use the logic in the schedule* methods on semimock instances
    protected ScheduledActivity createEmptyScheduledActivityFor(PlannedActivity plannedActivity) {
        ScheduledActivity event = new ScheduledActivity();
        event.setPlannedActivity(plannedActivity);
        return event;
    }

    private void avoidBlackoutDates(ScheduledStudySegment studySegment, Site site) {
        List<ScheduledActivity> listOfEvents = studySegment.getActivities();
        for (ScheduledActivity event : listOfEvents) {
            avoidBlackoutDates(event, site);
        }
    }

    public void avoidBlackoutDates(ScheduledActivity event) {
        avoidBlackoutDates(event,
                event.getScheduledStudySegment().getScheduledCalendar().getAssignment().getStudySite().getSite());
    }

    private void avoidBlackoutDates(ScheduledActivity event, Site site) {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        Calendar dateCalendar = Calendar.getInstance();
        Date date = event.getActualDate();
        dateCalendar.setTime(date);

        int year = dateCalendar.get(Calendar.YEAR);
        Calendar holidayCalendar = Calendar.getInstance();
        List<BlackoutDate> holidayList = site.getBlackoutDates();

        for (BlackoutDate blackoutDate : holidayList) {
            // TODO: instanceof indicates abstraction failure -- this logic should be in each BlackoutDate class
            if (blackoutDate instanceof SpecificDateBlackout) {
                //month needs to be decremented, because we are using 00 for January in the Calendar
                SpecificDateBlackout h = (SpecificDateBlackout) blackoutDate;
                if (h.getYear() == null) {
                    holidayCalendar.set(year, h.getMonth(), h.getDay());
                } else {
                    holidayCalendar.set(h.getYear(), h.getMonth(), h.getDay());
                }
                String originalDateFormatted = df.format(date.getTime());
                String holidayDateFormatted = df.format(holidayCalendar.getTime());
                if (originalDateFormatted.equals(holidayDateFormatted)) {
                    shiftToAvoidBlackoutDate(event, site, blackoutDate.getDescription());
                }
            } else if (blackoutDate instanceof WeekdayBlackout) {
                WeekdayBlackout dayOfTheWeek = (WeekdayBlackout) blackoutDate;
                int intValueOfTheDay = dayOfTheWeek.getDayOfTheWeekInteger();
                if (dateCalendar.get(Calendar.DAY_OF_WEEK) == intValueOfTheDay) {
                    shiftToAvoidBlackoutDate(event, site, blackoutDate.getDescription());
                }
            } else if (blackoutDate instanceof RelativeRecurringBlackout) {
                RelativeRecurringBlackout relativeRecurringHoliday = (RelativeRecurringBlackout) blackoutDate;
                Integer numberOfTheWeek = relativeRecurringHoliday.getWeekNumber();
                Integer month = relativeRecurringHoliday.getMonth();
                int dayOfTheWeekInt = relativeRecurringHoliday.getDayOfTheWeekInteger();
                List<Date> dates = findDaysOfWeekInMonth(year, month, dayOfTheWeekInt);
                //-1, since we start from position 0 in the list
                Date specificDay = dates.get(numberOfTheWeek - 1);

                String originalDateFormatted = df.format(date.getTime());
                String holidayDateFormatted = df.format(specificDay);
                if (originalDateFormatted.equals(holidayDateFormatted)) {
                    shiftToAvoidBlackoutDate(event, site, blackoutDate.getDescription());
                }
            }
        }
    }

    // package level for testing
    void shiftToAvoidBlackoutDate(ScheduledActivity event, Site site, String reason) {
        ScheduledActivityState newState = event.getCurrentState().getMode().createStateInstance(
                shiftDayByOne(event.getActualDate()), String.format("Rescheduled: %s", reason));
        event.changeState(newState);
        avoidBlackoutDates(event, site);
    }

    /**
     * Finds all the dates which represent the particular day of the week (Monday, Tuesday, etc.)
     * in the specified month.
     */
    // package level for testing
    List<Date> findDaysOfWeekInMonth(int year, int month, int dayOfTheWeek) {
        List<Date> matches = new ArrayList<Date>();
        Calendar search = Calendar.getInstance();
        search.set(year, month, 1);
        while (search.get(Calendar.MONTH) == month
                && search.get(Calendar.DAY_OF_MONTH) <= search.getActualMaximum(Calendar.DAY_OF_MONTH)) {
            System.out.println(search.getTime());
            if (search.get(Calendar.DAY_OF_WEEK) == dayOfTheWeek) {
                matches.add(search.getTime());
            }
            search.add(Calendar.DAY_OF_MONTH, 1);
        }
        return matches;
    }

    // package level for testing
    Date shiftDayByOne(Date date) {
        java.sql.Timestamp timestampTo = new java.sql.Timestamp(date.getTime());
        long oneDay = 24 * 60 * 60 * 1000;
        timestampTo.setTime(timestampTo.getTime() + oneDay);
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        String dateString = df.format(timestampTo);
        Date d1;
        try {
            d1 = df.parse(dateString);
        } catch (ParseException e) {
            log.debug("Exception " + e);
            d1 = date;
        }
        return d1;
    }

    private Date idealDate(int studySegmentDay, Date startDate) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(startDate);

        cal.add(Calendar.DAY_OF_YEAR, studySegmentDay - 1);
        return cal.getTime();
    }

    public StudySubjectAssignment takeSubjectOffStudy(StudySubjectAssignment studyAssignment, Date offStudyDate) {
        ScheduledCalendar calendar = studyAssignment.getScheduledCalendar();

        List<ScheduledActivity> upcomingScheduledActivities = getPotentialUpcomingEvents(calendar, offStudyDate);

        for (ScheduledActivity event : upcomingScheduledActivities) {
            if (ScheduledActivityMode.SCHEDULED == event.getCurrentState().getMode()) {
                event.changeState(ScheduledActivityMode.CANCELED
                        .createStateInstance(event.getCurrentState().getDate(), "Off Study"));
            } else if (ScheduledActivityMode.CONDITIONAL == event.getCurrentState().getMode()) {
                event.changeState(ScheduledActivityMode.NOT_APPLICABLE
                        .createStateInstance(event.getCurrentState().getDate(), "Off Study"));
            }
        }

        studyAssignment.setEndDate(offStudyDate);
        subjectDao.save(studyAssignment.getSubject());
        return studyAssignment;
    }

    private List<ScheduledActivity> getPotentialUpcomingEvents(ScheduledCalendar calendar, Date offStudyDate) {
        List<ScheduledActivity> upcomingScheduledActivities = new ArrayList<ScheduledActivity>();
        for (ScheduledStudySegment studySegment : calendar.getScheduledStudySegments()) {
            if (!studySegment.isComplete()) {
                Map<Date, List<ScheduledActivity>> eventsByDate = studySegment.getActivitiesByDate();
                for (Date date : eventsByDate.keySet()) {
                    List<ScheduledActivity> events = eventsByDate.get(date);
                    for (ScheduledActivity event : events) {
                        if ((offStudyDate.before(event.getActualDate())
                                || offStudyDate.equals(event.getActualDate()))
                                && (ScheduledActivityMode.SCHEDULED == event.getCurrentState().getMode()
                                        || ScheduledActivityMode.CONDITIONAL == event.getCurrentState()
                                                .getMode())) {
                            upcomingScheduledActivities.add(event);
                        }
                    }
                }
            }
        }
        return upcomingScheduledActivities;
    }

    public void updatePopulations(StudySubjectAssignment assignment, Set<Population> newPopulations) {
        log.debug("updating populations for {} to {}", assignment, newPopulations);
        if (newPopulations == null)
            newPopulations = Collections.emptySet();
        for (Iterator<Population> it = assignment.getPopulations().iterator(); it.hasNext();) {
            Population currentPopulation = it.next();
            if (!newPopulations.contains(currentPopulation)) {
                removePopulation(assignment, currentPopulation, it);
            }
        }
        for (Population newPopulation : newPopulations) {
            if (!assignment.getPopulations().contains(newPopulation)) {
                addPopulation(assignment, newPopulation);
            }
        }
    }

    private void removePopulation(StudySubjectAssignment assignment, Population toRemove,
            Iterator<Population> iterator) {
        log.debug("removing population {} from {}", toRemove.getAbbreviation(), assignment);
        iterator.remove();
        for (ScheduledStudySegment segment : assignment.getScheduledCalendar().getScheduledStudySegments()) {
            segment.unschedulePopulationEvents("Subject removed from population " + toRemove.getName(), toRemove);
        }
    }

    private void addPopulation(StudySubjectAssignment assignment, Population toAdd) {
        log.debug("adding population {} for {}", toAdd.getAbbreviation(), assignment);
        assignment.addPopulation(toAdd);
        for (ScheduledStudySegment segment : assignment.getScheduledCalendar().getScheduledStudySegments()) {
            if (!segment.isComplete()) {
                StudySegment amendedSourceSegment = amendmentService.getAmendedNode(segment.getStudySegment(),
                        assignment.getCurrentAmendment());
                for (Period period : amendedSourceSegment.getPeriods()) {
                    schedulePeriod(period, assignment.getCurrentAmendment(),
                            "Subject added to population " + toAdd.getName(), toAdd, segment);
                }
            }
        }
    }

    ////// CONFIGURATION

    @Required
    public void setSubjectDao(SubjectDao subjectDao) {
        this.subjectDao = subjectDao;
    }

    @Required
    public void setAmendmentService(AmendmentService amendmentService) {
        this.amendmentService = amendmentService;
    }

    @SuppressWarnings("unchecked")
    public Subject findSubjectByPersonId(final String mrn) {
        Subject subject = subjectDao.findSubjectByPersonId(mrn);
        if (subject != null) {
            Hibernate.initialize(subject.getAssignments());
            List<StudySubjectAssignment> studySubjectAssignments = subject.getAssignments();
            for (StudySubjectAssignment studySubjectAssignment : studySubjectAssignments) {
                Hibernate.initialize(studySubjectAssignment.getScheduledCalendar());
                if (studySubjectAssignment.getScheduledCalendar() != null) {
                    Hibernate.initialize(studySubjectAssignment.getScheduledCalendar().getScheduledStudySegments());
                }
            }
        }

        return subject;

    }

    @SuppressWarnings("unchecked")
    /**
     * Searches all the subjects in the system for those that match the given
     * criteria.  Returns a list of transient Subject elements containing just the
     * matching activities.
     */
    public List<Subject> getFilteredSubjects(String subjSearch) {
        return subjectDao.getSubjectsBySearchText(subjSearch);
    }

    public List<Subject> findSubjects(Subject searchCriteria) {
        if (searchCriteria == null)
            return null;

        validateSubjectAttributes(searchCriteria.getPersonId(), searchCriteria.getFirstName(),
                searchCriteria.getLastName(), searchCriteria.getDateOfBirth(), searchCriteria.getGender());

        if (searchCriteria.getPersonId() != null) {
            Subject subject = subjectDao.findSubjectByPersonId(searchCriteria.getPersonId());
            if (subject == null) {
                return Collections.emptyList();
            } else {
                return Collections.singletonList(subject);
            }
        } else {
            List<Subject> subjects = subjectDao.findSubjectByFirstNameLastNameAndDoB(searchCriteria.getFirstName(),
                    searchCriteria.getLastName(), searchCriteria.getDateOfBirth());

            if (subjects == null) {
                return Collections.emptyList();
            } else {
                return subjects;
            }
        }
    }

    public Subject findSubject(Subject searchCriteria) {
        List<Subject> subjects = findSubjects(searchCriteria);
        if (subjects.isEmpty()) {
            return null;
        } else if (subjects.size() == 1) {
            return subjects.get(0);
        } else {
            throw new StudyCalendarValidationException(
                    "Multiple subjects found for %s, %s.  With a birth date of %s", searchCriteria.getLastName(),
                    searchCriteria.getFirstName(), searchCriteria.getDateOfBirth());
        }
    }

    private void validateSubjectAttributes(String personId, String firstName, String lastName, Date birthDate,
            Gender gender) {
        if (StringUtils.isEmpty(personId)) {
            if (StringUtils.isEmpty(firstName)) {
                throw new StudyCalendarValidationException("Subject first name is required if person id is empty");
            } else if (StringUtils.isEmpty(lastName)) {
                throw new StudyCalendarValidationException("Subject last name is required if person id is empty");
            } else if (birthDate == null) {
                throw new StudyCalendarValidationException("Subject birth date is required if person id is empty");
            }
        }
        if (gender == null) {
            throw new StudyCalendarValidationException("Subject gender is required");
        }
    }

    private static class DatabaseEventOrderComparator implements Comparator<ScheduledActivity> {
        public static final Comparator<? super ScheduledActivity> INSTANCE = new DatabaseEventOrderComparator();

        public int compare(ScheduledActivity e1, ScheduledActivity e2) {
            int dateCompare = e1.getIdealDate().compareTo(e2.getIdealDate());
            if (dateCompare != 0)
                return dateCompare;

            if (e1.getPlannedActivity() == null && e2.getPlannedActivity() == null) {
                return 0;
            } else if (e1.getPlannedActivity() == null) {
                return -1;
            } else if (e2.getPlannedActivity() == null) {
                return 1;
            }

            return e1.getPlannedActivity().getId().compareTo(e2.getPlannedActivity().getId());
        }
    }

}