Coverage Report - org.kuali.student.r2.core.class1.appointment.service.impl.AppointmentServiceImplHelper
 
Classes in this File Line Coverage Branch Coverage Complexity
AppointmentServiceImplHelper
0%
0/299
0%
0/124
2.841
 
 1  
 /**
 2  
  * Copyright 2012 The Kuali Foundation Licensed under the
 3  
  * Educational Community License, Version 2.0 (the "License"); you may
 4  
  * not use this file except in compliance with the License. You may
 5  
  * obtain a copy of the License at
 6  
  *
 7  
  * http://www.osedu.org/licenses/ECL-2.0
 8  
  *
 9  
  * Unless required by applicable law or agreed to in writing,
 10  
  * software distributed under the License is distributed on an "AS IS"
 11  
  * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 12  
  * or implied. See the License for the specific language governing
 13  
  * permissions and limitations under the License.
 14  
  *
 15  
  * Created by Charles on 4/14/12
 16  
  */
 17  
 package org.kuali.student.r2.core.class1.appointment.service.impl;
 18  
 
 19  
 import org.kuali.student.r2.common.dto.ContextInfo;
 20  
 import org.kuali.student.r2.common.dto.StatusInfo;
 21  
 import org.kuali.student.r2.common.exceptions.*;
 22  
 import org.kuali.student.r2.common.util.constants.AtpServiceConstants;
 23  
 import org.kuali.student.r2.core.appointment.constants.AppointmentServiceConstants;
 24  
 import org.kuali.student.r2.core.appointment.dto.AppointmentInfo;
 25  
 import org.kuali.student.r2.core.appointment.dto.AppointmentSlotInfo;
 26  
 import org.kuali.student.r2.core.appointment.dto.AppointmentWindowInfo;
 27  
 import org.kuali.student.r2.core.class1.appointment.dao.AppointmentDao;
 28  
 import org.kuali.student.r2.core.class1.appointment.dao.AppointmentSlotDao;
 29  
 import org.kuali.student.r2.core.class1.appointment.dao.AppointmentWindowDao;
 30  
 import org.kuali.student.r2.core.class1.appointment.model.AppointmentEntity;
 31  
 import org.kuali.student.r2.core.class1.appointment.model.AppointmentSlotEntity;
 32  
 import org.kuali.student.r2.core.class1.appointment.model.AppointmentWindowEntity;
 33  
 import org.kuali.student.r2.core.population.service.PopulationService;
 34  
 
 35  
 import javax.annotation.Resource;
 36  
 import javax.jws.WebParam;
 37  
 import java.util.ArrayList;
 38  
 import java.util.Calendar;
 39  
 import java.util.Date;
 40  
 import java.util.List;
 41  
 
 42  
 /**
 43  
  * This class //TODO ...
 44  
  *
 45  
  * @author Kuali Student Team
 46  
  */
 47  
 public class AppointmentServiceImplHelper {
 48  0
     private static int MILLIS_IN_SECOND = 1000;
 49  0
     private static int MILLIS_IN_MINUTE = MILLIS_IN_SECOND * 60;
 50  0
     private static int MINUTES_IN_HOUR = 60;
 51  
     
 52  
     private AppointmentWindowDao appointmentWindowDao;
 53  
     private AppointmentSlotDao appointmentSlotDao;
 54  
     private AppointmentDao appointmentDao;
 55  
     private PopulationService populationService;
 56  
 
 57  0
     public AppointmentServiceImplHelper() {
 58  0
     }
 59  
 
 60  
     public void setAppointmentWindowDao(AppointmentWindowDao appointmentWindowDao) {
 61  0
         this.appointmentWindowDao = appointmentWindowDao;
 62  0
     }
 63  
 
 64  
     public void setAppointmentSlotDao(AppointmentSlotDao appointmentSlotDao) {
 65  0
         this.appointmentSlotDao = appointmentSlotDao;
 66  0
     }
 67  
 
 68  
     public void setAppointmentDao(AppointmentDao appointmentDao) {
 69  0
         this.appointmentDao = appointmentDao;
 70  0
     }
 71  
 
 72  
     public void setPopulationService(PopulationService populationService) {
 73  0
         this.populationService = populationService;
 74  0
     }
 75  
 
 76  
     /*
 77  
     * This is pulled out so other methods can call this without the transactional behavior.
 78  
     */
 79  
     public AppointmentInfo createAppointmentNoTransact(String personId, String appointmentSlotId, String appointmentTypeKey, AppointmentInfo appointmentInfo, @WebParam(name = "contextInfo") ContextInfo contextInfo) throws DataValidationErrorException, DoesNotExistException, InvalidParameterException, MissingParameterException, OperationFailedException, PermissionDeniedException, ReadOnlyException {
 80  0
         AppointmentEntity  appointmentEntity = new AppointmentEntity(appointmentInfo);
 81  0
         appointmentEntity.setCreateId(contextInfo.getPrincipalId());
 82  0
         appointmentEntity.setCreateTime(contextInfo.getCurrentDate());
 83  0
         appointmentEntity.setUpdateId(contextInfo.getPrincipalId());
 84  0
         appointmentEntity.setUpdateTime(contextInfo.getCurrentDate());
 85  
         // TODO: Determine if there should be a check between apptType/slotId and apptInfo counterparts
 86  
         // Need to manually set the entity since appointmentInfo only has an id for its corresponding AppointmentSlot
 87  0
         AppointmentSlotEntity slotEntity = appointmentSlotDao.find(appointmentSlotId);
 88  0
         if (null == slotEntity) {
 89  0
             throw new DoesNotExistException(appointmentSlotId);
 90  
         }
 91  0
         appointmentEntity.setSlotEntity(slotEntity); // This completes the initialization of appointmentSlotEntity
 92  
 
 93  0
         appointmentDao.persist(appointmentEntity);
 94  0
         return appointmentEntity.toDto();
 95  
     }
 96  
 
 97  
     // ------------------------------ Slot creation ------------------------
 98  
     public List<AppointmentSlotInfo> createOneSlotPerWindow(AppointmentWindowEntity apptWin, ContextInfo contextInfo)
 99  
             throws InvalidParameterException, OperationFailedException, MissingParameterException, PermissionDeniedException, ReadOnlyException, DataValidationErrorException, DoesNotExistException {
 100  0
         AppointmentSlotInfo slotInfo = new AppointmentSlotInfo();
 101  0
         slotInfo.setStartDate(apptWin.getStartDate());
 102  
         // Find end date
 103  0
         _setEndDateToDropDate(slotInfo);
 104  
 
 105  
         // One slot per appointment window has no set end date and is defined by the end of the period
 106  0
         slotInfo.setTypeKey(AppointmentServiceConstants.APPOINTMENT_SLOT_TYPE_OPEN_KEY);
 107  0
         slotInfo.setStateKey(AppointmentServiceConstants.APPOINTMENT_SLOTS_STATE_ACTIVE_KEY);
 108  0
         List<AppointmentSlotInfo> slots = new ArrayList<AppointmentSlotInfo>();
 109  0
         slots.add(slotInfo);
 110  0
         return slots;
 111  
     }
 112  
     // ------------------------------- delete public methods ----------------------------
 113  
     /**
 114  
      * Helper that is used both by deleteAppointmentWindow and deleteAppointmentSlotsByWindow
 115  
      * @param windowEntity An appointment window entity
 116  
      * @param shouldDeleteSlots true, if you want slots to also be deleted
 117  
      */
 118  
     public void deleteAppointmentsByWindow(AppointmentWindowEntity windowEntity, boolean shouldDeleteSlots) {
 119  0
         List<AppointmentSlotEntity> slotList = _fetchSlotEntitiesByWindows(windowEntity.getId());
 120  0
         if (slotList != null) {
 121  
             // Delete appointments, if any
 122  0
             _deleteAppointmentsBySlotList_(slotList);
 123  
             // Delete slots, if any
 124  0
             _deleteAppointmentSlots(slotList);
 125  
         }
 126  0
         changeApptWinState(windowEntity, AppointmentServiceConstants.APPOINTMENT_WINDOW_STATE_DRAFT_KEY);
 127  0
     }
 128  
 
 129  
     public void deleteAppointmentSlotsByWindowCascading(AppointmentWindowEntity windowEntity) {
 130  0
         deleteAppointmentsByWindow(windowEntity, true); // also, delete slots
 131  0
     }
 132  
 
 133  
     /**
 134  
      * Deletes appointment windows, slots, and appointments.
 135  
      * @param windowEntity The window (and its dependent parts) to be deleted.
 136  
      */
 137  
     public void deleteAppointmentWindowCascading(AppointmentWindowEntity windowEntity) {
 138  0
         deleteAppointmentSlotsByWindowCascading(windowEntity);
 139  0
         appointmentWindowDao.remove(windowEntity);
 140  
         // No need to update the state since the window is gone
 141  0
     }
 142  
 
 143  
     public int deleteAppointmentsBySlotCascading(String slotId) {
 144  0
         List<AppointmentEntity> apptList = appointmentDao.getAppointmentsBySlotId(slotId);
 145  0
         if (apptList != null) {
 146  0
             for (AppointmentEntity appt: apptList) {
 147  0
                 appointmentDao.remove(appt);
 148  
             }
 149  0
             return apptList.size();
 150  
         }
 151  0
         return -1; // didn't find any appointments (does this really happen?)
 152  
     }
 153  
 
 154  
     /**
 155  
      * Changes the state of the appointment window (to draft or assigned)
 156  
      * Precondition: apptWinId and stateKey are valid
 157  
      * @param windowEntity The appointment window ID in the DB
 158  
      * @param stateKey Either AppointmentServiceConstants.APPOINTMENT_WINDOW_STATE_DRAFT_KEY or
 159  
      *                 AppointmentServiceConstants.APPOINTMENT_WINDOW_STATE_ASSIGNED_KEY
 160  
      */
 161  
     public void changeApptWinState(AppointmentWindowEntity windowEntity, String stateKey) {
 162  0
         windowEntity.setApptWindowState(stateKey);
 163  0
         appointmentWindowDao.update(windowEntity);
 164  0
     }
 165  
 
 166  
     /**
 167  
      * Creates a list of AppointmentSlotInfo (but does not persist in DB).
 168  
      * @param apptWinInfo Appointment Window
 169  
      * @param contextInfo Context info
 170  
      * @return Object with 3 elements.  Object[0] is a list of AppointmentSlotInfo objects, Object[1] is number allocated
 171  
      * (only for max allocation, otherwise 0 for uniform), and Object[2] is number unallocated (again, only for max
 172  
      * allocation, otherwise 0 for uniform)
 173  
      */
 174  
     public Object[] createMultiSlots(AppointmentWindowInfo apptWinInfo, ContextInfo contextInfo)
 175  
             throws InvalidParameterException, MissingParameterException, DoesNotExistException,
 176  
                    OperationFailedException, PermissionDeniedException {
 177  0
         String atpDurationTypeKey = apptWinInfo.getSlotRule().getSlotStartInterval().getAtpDurationTypeKey();
 178  0
         if (!AtpServiceConstants.DURATION_MINUTES_TYPE_KEY.equals(atpDurationTypeKey)) {
 179  
             // Currently, only support minutes
 180  0
             throw new InvalidParameterException("Only kuali.atp.duration.Minutes is implemented");
 181  
         }
 182  0
         String apptWinTypeKey = apptWinInfo.getTypeKey();
 183  0
         if (! (AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_MAX_KEY.equals(apptWinTypeKey) ||
 184  
               (AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_UNIFORM_KEY.equals(apptWinTypeKey)))) {
 185  0
             throw new InvalidParameterException("Supported types: MAX and UNIFORM");
 186  
         }
 187  
         // Get parameters ready
 188  0
         Calendar startDate = _convertDateToCalendar(apptWinInfo.getStartDate());
 189  0
         Calendar endDate = _convertDateToCalendar(apptWinInfo.getEndDate());
 190  0
         int maxPerSlot = apptWinInfo.getMaxAppointmentsPerSlot() == null ? 0 : apptWinInfo.getMaxAppointmentsPerSlot();
 191  0
         int totalStudents = -1;
 192  0
         if (AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_MAX_KEY.equals(apptWinInfo.getTypeKey())) {
 193  0
             totalStudents = _computeTotalStudents(apptWinInfo, contextInfo);
 194  
         }
 195  
 
 196  0
         int startTimeInMinutes = _computeMinuteOffsetSinceMidnight(apptWinInfo.getSlotRule().getStartTimeOfDay().getMilliSeconds());
 197  0
         int endTimeInMinutes =  _computeMinuteOffsetSinceMidnight(apptWinInfo.getSlotRule().getEndTimeOfDay().getMilliSeconds());
 198  
         // TODO: Current implementation only supports minutes.  Except thrown at start of this method for any other
 199  0
         int startIntervalInMinutes = apptWinInfo.getSlotRule().getSlotStartInterval().getTimeQuantity();
 200  0
         int durationInMinutes = -1; // TODO: Currently, unsupported
 201  0
         List<Integer> weekdays = apptWinInfo.getSlotRule().getWeekdays();
 202  
         
 203  
         // The big call!  Compute the slot times
 204  0
         Object[] result = null;
 205  0
         if (AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_MAX_KEY.equals(apptWinTypeKey)) {
 206  0
             result = _computeSlotTimesMaxAllocation(startDate, endDate, totalStudents, maxPerSlot, startTimeInMinutes,
 207  
                                                     endTimeInMinutes, startIntervalInMinutes, 
 208  
                                                     durationInMinutes, weekdays);
 209  0
         } else if (AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_UNIFORM_KEY.equals(apptWinTypeKey)) {
 210  0
             result = _computeSlotTimesUniformAllocation(startDate, endDate, totalStudents, startTimeInMinutes,
 211  
                                                         endTimeInMinutes, startIntervalInMinutes, durationInMinutes, 
 212  
                                                         weekdays);
 213  
         }
 214  0
         List<AppointmentSlotInfo> slotList = new ArrayList<AppointmentSlotInfo>();
 215  0
         _computeApptSlotList(slotList, (List<Calendar>) result[0]);
 216  0
         Object[] result2 = {slotList, result[1], result[2]};
 217  0
         return result2;
 218  
     }
 219  
 
 220  
     public List<AppointmentSlotInfo> createAppointmentSlots(List<AppointmentSlotInfo> slotInfoList, String apptSlotTypeKey,
 221  
                                                               String apptWinId, ContextInfo contextInfo)
 222  
             throws InvalidParameterException, DataValidationErrorException, MissingParameterException,
 223  
                    DoesNotExistException, ReadOnlyException, PermissionDeniedException, OperationFailedException {
 224  0
         List<AppointmentSlotInfo> newSlotInfoList = new ArrayList<AppointmentSlotInfo>();
 225  0
         for (AppointmentSlotInfo slotInfo: slotInfoList) {
 226  0
             slotInfo.setAppointmentWindowId(apptWinId);
 227  0
             AppointmentSlotInfo newSlotInfo = createAppointmentSlot(apptWinId, apptSlotTypeKey, slotInfo, contextInfo);
 228  0
             newSlotInfoList.add(newSlotInfo);
 229  0
         }
 230  0
         return newSlotInfoList;
 231  
     }
 232  
 
 233  
     public AppointmentSlotInfo createAppointmentSlot(String appointmentWindowId, String appointmentSlotTypeKey,
 234  
                                                      AppointmentSlotInfo appointmentSlotInfo, ContextInfo contextInfo)
 235  
             throws DataValidationErrorException, DoesNotExistException, InvalidParameterException,
 236  
             MissingParameterException, OperationFailedException, PermissionDeniedException, ReadOnlyException {
 237  0
         AppointmentSlotEntity appointmentSlotEntity = new AppointmentSlotEntity(appointmentSlotTypeKey, appointmentSlotInfo);
 238  0
         appointmentSlotEntity.setCreateId(contextInfo.getPrincipalId());
 239  0
         appointmentSlotEntity.setCreateTime(contextInfo.getCurrentDate());
 240  0
         appointmentSlotEntity.setUpdateId(contextInfo.getPrincipalId());
 241  0
         appointmentSlotEntity.setUpdateTime(contextInfo.getCurrentDate());
 242  
         // Need to manually set the entity since appointmentSlotInfo only has an id for its corresponding AppointmentWindow
 243  0
         AppointmentWindowEntity windowEntity = appointmentWindowDao.find(appointmentWindowId);
 244  0
         if (null == windowEntity) {
 245  0
             throw new DoesNotExistException(appointmentWindowId);
 246  
         }
 247  0
         appointmentSlotEntity.setApptWinEntity(windowEntity); // This completes the initialization of appointmentSlotEntity
 248  0
         appointmentSlotDao.persist(appointmentSlotEntity);
 249  0
         return appointmentSlotEntity.toDto();
 250  
     }
 251  
 
 252  
     public long countAppointmentsByApptWinId(String apptWinId) {
 253  0
         long result = appointmentDao.countAppointmentsByWindowId(apptWinId);
 254  0
         return result;
 255  
     }
 256  
 
 257  
     public List<AppointmentInfo> getAppointmentsBySlotNoTransact(String appointmentSlotId, ContextInfo contextInfo) throws InvalidParameterException, MissingParameterException, OperationFailedException, PermissionDeniedException {
 258  0
         List<AppointmentEntity> apptEntityList = appointmentDao.getAppointmentsBySlotId(appointmentSlotId);
 259  0
         List<AppointmentInfo> apptInfoList = new ArrayList<AppointmentInfo>();
 260  0
         for (AppointmentEntity entity: apptEntityList) {
 261  0
             AppointmentInfo info = entity.toDto();
 262  0
             apptInfoList.add(info);
 263  0
         }
 264  0
         return apptInfoList;
 265  
     }
 266  
 
 267  
     // maxSizePerSlot is likely to be null except in the max case, so leave it as an object here
 268  
     public void generateAppointments(String apptWinId,
 269  
                                      String appointmentTypeKey, Integer maxSizePerSlot,
 270  
                                      List<String> students, List<AppointmentSlotInfo> slotInfoList,
 271  
                                      ContextInfo contextInfo,
 272  
                                      StatusInfo statusInfo) throws OperationFailedException, InvalidParameterException, DataValidationErrorException, MissingParameterException, DoesNotExistException, ReadOnlyException, PermissionDeniedException {
 273  0
         if (slotInfoList.isEmpty()) {
 274  0
             throw new OperationFailedException("No slots");
 275  
         }
 276  
         // May want to adjust message in the status info
 277  0
         if (appointmentTypeKey.equals(AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_ONE_SLOT_KEY)) {
 278  0
             _generateAppointmentsOneSlotCase(students, slotInfoList, statusInfo, contextInfo);
 279  0
         } else if (appointmentTypeKey.equals(AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_UNIFORM_KEY)) {
 280  0
             _generateAppointmentsUniformCase(students, slotInfoList, statusInfo, contextInfo);
 281  0
         } else if (appointmentTypeKey.equals(AppointmentServiceConstants.APPOINTMENT_WINDOW_TYPE_SLOTTED_MAX_KEY)) {
 282  0
             _generateAppointmentsMaxCase(apptWinId, students, maxSizePerSlot, slotInfoList, statusInfo, contextInfo);
 283  
         } else {
 284  0
             throw new OperationFailedException("Manual window allocation not supported");
 285  
         }
 286  0
     }
 287  
     // =====================================================================================
 288  
     // ------------------------------- PRIVATE METHODS -------------------------------------
 289  
     // =====================================================================================
 290  
     
 291  
     // ---------------------------- Slot generation private
 292  
     /**
 293  
      *
 294  
      * @param startDate When appointments can start (including hour/minute)
 295  
      * @param endDate When appointments end (an appointment can be scheduled exactly at that time)
 296  
      * @param totalStudents How many students we need slots for.  If -1, we won't care
 297  
      * @param maxPerSlot How many students can be allocated per slot
 298  
      * @param startTimeInMinutes Offset from midnight in minutes for start of business day
 299  
      *                           (doesn't account for daylight saving transitions)
 300  
      * @param endTimeInMinutes Offset from midnight in minutes for start of business day
 301  
      *                         (same caveat)
 302  
      * @param startIntervalInMinutes Difference in minutes
 303  
      * @param durationInMinutes How long an appointment lasts in minutes (currently ignored)
 304  
      * @param weekdays List of integers representing which days of the week business hours occur
 305  
      *                 (1 - Sunday, 2 - Monday, ..., 7 - Saturday).  Must be non-empty, non-null
 306  
      * @return Object[0] contains List<Calendar> for slot times in chronological order.  Object[1]
 307  
      *         contains Integer for number of students allocated, Object[2] contains an Integer for
 308  
      *         number of students unallocated.  Object[1] + Object[2] adds to totalStudents.
 309  
      */
 310  
     private Object [] _computeSlotTimesMaxAllocation(Calendar startDate, Calendar endDate, int totalStudents,
 311  
                                                      int maxPerSlot, int startTimeInMinutes, int endTimeInMinutes,
 312  
                                                      int startIntervalInMinutes, int durationInMinutes,
 313  
                                                      List<Integer> weekdays) throws InvalidParameterException {
 314  0
         if (totalStudents < 0) {
 315  0
             throw new InvalidParameterException("totalStudents should be positive");
 316  
         }
 317  0
         List<Calendar> slotTimes = new ArrayList<Calendar>();
 318  0
         if (weekdays.isEmpty()) {
 319  0
             Object[] result = {slotTimes, new Integer(0), new Integer(totalStudents)};
 320  0
             return result;
 321  
         }
 322  0
         Calendar date = _makeCopy(startDate);
 323  0
         _setTime(date, startTimeInMinutes); // Set to start of day
 324  0
         int numAllocated = 0; // Number of students accounted for by slots
 325  
         while (true) {
 326  
             // Exit if we're past the end date
 327  0
             if (endDate != null && date.after(endDate)) {
 328  0
                 break;
 329  
             }
 330  
             // Exit loop if we're allocated enough (but make sure we're checking for that)
 331  0
             if (numAllocated >= totalStudents) {
 332  0
                 break;
 333  
             }
 334  
 
 335  
             // -------------------- Now allocate slots for current day --------------------
 336  0
             Calendar endOfToday = _makeCopy(date);
 337  0
             _setTime(endOfToday, endTimeInMinutes);
 338  
             while (true) { // Iterate over slots for today
 339  
                 // Make sure first slot is >= startDate
 340  0
                 _setTimePastStartDate(date, startDate, startIntervalInMinutes);
 341  0
                 if (date.after(endOfToday) || date.after(endDate)) {
 342  0
                     break; // quit out of if we've reached the end of the day or final end date
 343  
                 }
 344  0
                 slotTimes.add(date); // Add next slot time
 345  0
                 if (maxPerSlot > 0) {
 346  0
                     numAllocated += maxPerSlot;
 347  
                 }
 348  0
                 if (numAllocated >= totalStudents) {
 349  0
                     break; // We have enough slots to account for all students, so quit
 350  
                 }
 351  
                 // Create next date
 352  0
                 date = _makeCopy(date, startIntervalInMinutes);
 353  
             }
 354  
             // Searches for the start of the next available business day
 355  0
             date = _getNextValidDate(date, startTimeInMinutes, weekdays, endDate);
 356  0
             if (date == null) {
 357  0
                 break;
 358  
             }
 359  0
         }
 360  
         // We haven't really allocated, just computed how many we *would* allocate.
 361  0
         if (numAllocated > totalStudents) {
 362  0
             numAllocated = totalStudents;
 363  
         }
 364  0
         int unallocated = totalStudents - numAllocated;
 365  0
         Object[] result = {slotTimes, new Integer(numAllocated), new Integer(unallocated)};
 366  0
         return result;
 367  
     }
 368  
 
 369  
     private Object [] _computeSlotTimesUniformAllocation(Calendar startDate, Calendar endDate, int totalStudents,
 370  
                                                          int startTimeInMinutes, int endTimeInMinutes,
 371  
                                                          int startIntervalInMinutes, int durationInMinutes,
 372  
                                                          List<Integer> weekdays) {
 373  0
         List<Calendar> slotTimes = new ArrayList<Calendar>();
 374  0
         if (weekdays.isEmpty()) {
 375  0
             Object[] result = {slotTimes, new Integer(0), new Integer(totalStudents)};
 376  0
             return result;
 377  
         }
 378  0
         Calendar date = _makeCopy(startDate);
 379  0
         _setTime(date, startTimeInMinutes); // Set to start of day
 380  
         while (true) {
 381  
             // Exit if we're past the end date
 382  0
             if (endDate != null && date.after(endDate)) {
 383  0
                 break;
 384  
             }
 385  
             // -------------------- Now allocate slots for current day --------------------
 386  0
             Calendar endOfToday = _makeCopy(date);
 387  0
             _setTime(endOfToday, endTimeInMinutes);
 388  
             while (true) { // Iterate over slots for today
 389  
                 // Make sure first slot is >= startDate
 390  0
                 _setTimePastStartDate(date, startDate, startIntervalInMinutes);
 391  0
                 if (date.after(endOfToday) || date.after(endDate)) {
 392  0
                     break; // quit out of if we've reached the end of the day or final end date
 393  
                 }
 394  0
                 slotTimes.add(date); // Add next slot time
 395  
                 // Create next date
 396  0
                 date = _makeCopy(date, startIntervalInMinutes);
 397  
             }
 398  
             // Searches for the start of the next available business day
 399  0
             date = _getNextValidDate(date, startTimeInMinutes, weekdays, endDate);
 400  0
             if (date == null) {
 401  0
                 break;
 402  
             }
 403  0
         }
 404  
         // We haven't really allocated, just computed how many we *would* allocate.
 405  0
         Object[] result = {slotTimes, new Integer(totalStudents), new Integer(0)};
 406  0
         return result;
 407  
     }
 408  
 
 409  
     private void _computeApptSlotList(List<AppointmentSlotInfo> slotList, List<Calendar> slotTimes) {
 410  0
         for (Calendar date: slotTimes) {    
 411  0
             AppointmentSlotInfo slotInfo = _createApptSlotFromDate(date);
 412  0
             slotList.add(slotInfo);
 413  0
         }
 414  0
     }
 415  
     
 416  
     private AppointmentSlotInfo _createApptSlotFromDate(Calendar day) {
 417  0
         AppointmentSlotInfo slotInfo = new AppointmentSlotInfo();
 418  0
         slotInfo.setStateKey(AppointmentServiceConstants.APPOINTMENT_SLOTS_STATE_ACTIVE_KEY);
 419  0
         slotInfo.setTypeKey(AppointmentServiceConstants.APPOINTMENT_SLOT_TYPE_OPEN_KEY); // TODO: verify this is valid
 420  0
         Date date = day.getTime();
 421  0
         slotInfo.setStartDate(date);
 422  0
         _setEndDateToDropDate(slotInfo);
 423  0
         return slotInfo;
 424  
     }
 425  
     
 426  
     private int _computeTotalStudents(AppointmentWindowInfo apptWinInfo, ContextInfo contextInfo) throws InvalidParameterException, MissingParameterException, DoesNotExistException, PermissionDeniedException, OperationFailedException {
 427  0
         List<String> ids = populationService.getMembers(apptWinInfo.getAssignedPopulationId(), contextInfo);
 428  0
         if (ids != null) {
 429  0
             return ids.size();
 430  
         } else {
 431  0
             return 0;  // TODO: Perhaps do something else?  Throw an exception?
 432  
         }
 433  
     }
 434  
 
 435  
 
 436  
     // ---------------------------------- Calendar private methods ----------------------------------
 437  
     /**
 438  
      * Computes minutes since start of day (midnight).  We don't need millisecond granularity.
 439  
      * Assumption: We ignore daylight saving.  Thus, 9 AM is always 9 * 60 = 540 minutes from midnight.
 440  
      * @param milliseconds Milliseconds since start of day
 441  
      * @return Minutes since start of day
 442  
      */
 443  
     private int _computeMinuteOffsetSinceMidnight(Long milliseconds) {
 444  0
         int minutes = (int) (milliseconds / MILLIS_IN_MINUTE);
 445  0
         return minutes;
 446  
     }
 447  
 
 448  
     private Calendar _convertDateToCalendar(Date date) {
 449  0
         if (date == null) {
 450  0
             return null;
 451  
         }
 452  0
         Calendar result = Calendar.getInstance();
 453  0
         result.setTime(date);
 454  0
         return result;
 455  
     }
 456  
 
 457  
     private void _zeroOutHoursMinutesEtc(Calendar cal) {
 458  0
         cal.set(Calendar.HOUR_OF_DAY, 0);
 459  0
         cal.set(Calendar.MINUTE, 0);
 460  0
         cal.set(Calendar.SECOND, 0);
 461  0
         cal.set(Calendar.MILLISECOND, 0);
 462  0
     }
 463  
 
 464  
     private void _zeroOutSecondsMillis(Calendar cal) {
 465  0
         cal.set(Calendar.SECOND, 0);
 466  0
         cal.set(Calendar.MILLISECOND, 0);
 467  0
     }
 468  
 
 469  0
     private static int DAYS_OF_WEEK[] = {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
 470  
             Calendar.FRIDAY, Calendar.SATURDAY};
 471  
 
 472  
     // weekdays contains values 1 -7 (1: Sunday, ..., 7: Saturday)
 473  
     private boolean _isDayInWeekdays(Calendar date, List<Integer> weekdays) {
 474  0
         int dayOfWeek = date.get(Calendar.DAY_OF_WEEK);
 475  0
         for (int i = 0; i < weekdays.size(); i++) {
 476  0
             int adjustedDayOfWeek = weekdays.get(i) - 1; // Shift to 0..6
 477  0
             if (dayOfWeek == DAYS_OF_WEEK[adjustedDayOfWeek]) {
 478  0
                 return true;
 479  
             }
 480  
         }
 481  0
         return false;
 482  
     }
 483  
 
 484  
     private boolean _isSameDay(Calendar first, Calendar second) {
 485  0
         return first.get(Calendar.YEAR) == second.get(Calendar.YEAR) &&
 486  
                 first.get(Calendar.MONTH) == second.get(Calendar.MONTH) &&
 487  
                 first.get(Calendar.DAY_OF_MONTH) == second.get(Calendar.DAY_OF_MONTH);
 488  
     }
 489  
     
 490  
     private Calendar _makeCopy(Calendar date) {
 491  0
         Calendar copy = Calendar.getInstance();
 492  0
         copy.setTimeInMillis(date.getTimeInMillis());
 493  0
         return copy;
 494  
     }
 495  
 
 496  
     private Calendar _makeCopy(Calendar date, int offsetInMinutes) {
 497  0
         Calendar copy = Calendar.getInstance();
 498  0
         copy.setTimeInMillis(date.getTimeInMillis());
 499  0
         copy.add(Calendar.MINUTE, offsetInMinutes);
 500  0
         return copy;
 501  
     }
 502  
     
 503  
     private void _setTime(Calendar date, int minutesSinceMidnight) {
 504  0
         int hours = minutesSinceMidnight / MINUTES_IN_HOUR;
 505  0
         int minutes = minutesSinceMidnight % MINUTES_IN_HOUR;
 506  0
         date.set(Calendar.HOUR_OF_DAY, hours);
 507  0
         date.set(Calendar.MINUTE, minutes);
 508  0
     }
 509  
     
 510  
     private Calendar _getNextValidDate(Calendar date, int startOfDayInMinutes, List<Integer> weekdays,
 511  
                                        Calendar endDate) {
 512  0
         Calendar copy = _makeCopy(date);
 513  0
         _setTime(copy, startOfDayInMinutes);
 514  0
         copy.add(Calendar.DAY_OF_MONTH, 1); // Add a day
 515  0
         int loopCount = 0;
 516  
         while (true) {
 517  0
             if (endDate != null && copy.after(endDate)) {
 518  0
                 return null;
 519  
             }
 520  0
             if (_isDayInWeekdays(copy, weekdays)) {
 521  0
                 return copy;
 522  
             }
 523  0
             copy.add(Calendar.DAY_OF_MONTH, 1); // Add a day
 524  0
             loopCount++;
 525  0
             if (loopCount > 10) {
 526  
                 // TODO: Figure this out (should it be taken care of)
 527  
             }
 528  
         }
 529  
     }
 530  
     
 531  
     private boolean _sameDayMonthYear(Calendar date, Calendar startDate) {
 532  0
         return date.get(Calendar.DAY_OF_MONTH) == startDate.get(Calendar.DAY_OF_MONTH) &&
 533  
                 date.get(Calendar.MONTH) == startDate.get(Calendar.MONTH) &&
 534  
                 date.get(Calendar.YEAR) == startDate.get(Calendar.YEAR);
 535  
     }
 536  
     
 537  
     private void _setTimePastStartDate(Calendar date, Calendar startDate, int startIntervalInMinutes) {
 538  0
         if (_sameDayMonthYear(date, startDate)) {
 539  0
             while (date.before(startDate)) {
 540  0
                 date.add(Calendar.MINUTE, startIntervalInMinutes);
 541  
             }
 542  
         }
 543  0
     }
 544  
     // ---------------------- DELETE Private Methods --------------------------------
 545  
     private List<AppointmentSlotEntity> _fetchSlotEntitiesByWindows(String apptWinId) {
 546  0
         List<AppointmentSlotEntity> list = appointmentSlotDao.getSlotsByWindowIdSorted(apptWinId);
 547  0
         return list;
 548  
     }
 549  
 
 550  
     private void _deleteAppointmentSlots(List<AppointmentSlotEntity> slotList) {
 551  0
         if (slotList != null) {
 552  0
             for (AppointmentSlotEntity entity: slotList) {
 553  
                 // Use dao since deleteAppointmentSlot is transactional per slot deletion (we want the entire
 554  
                 // delete operation to be one big transaction (cclin asks: correct?)
 555  0
                 appointmentSlotDao.remove(entity);
 556  
             }
 557  
         }
 558  0
     }
 559  
 
 560  
     /**
 561  
      * Deletes all appointments associated with this slot list.
 562  
      * Precondition: Don't call directly by public methods--doesn't change state of window
 563  
      * @param slotList Assume not-null
 564  
      */
 565  
     private void _deleteAppointmentsBySlotList_(List<AppointmentSlotEntity> slotList) {
 566  0
         for (AppointmentSlotEntity slotEntity: slotList) {
 567  0
             deleteAppointmentsBySlotCascading(slotEntity.getId());
 568  
         }
 569  0
     }
 570  
     // ----------------------- Generate appts private methods ---------------------
 571  
 
 572  
     private void _generateAppointmentsOneSlotCase(List<String> studentIds, List<AppointmentSlotInfo> slotInfoList,
 573  
                                                   StatusInfo statusInfo, ContextInfo contextInfo)
 574  
             throws InvalidParameterException, MissingParameterException, DoesNotExistException,
 575  
                    PermissionDeniedException, OperationFailedException, DataValidationErrorException, ReadOnlyException {
 576  0
         String slotId = slotInfoList.get(0).getId();  // Only one slot in the one slot case
 577  0
         for (String studentId: studentIds) {
 578  0
             AppointmentInfo apptInfo = _createAppointmentInfo(studentId, slotId);
 579  0
             createAppointmentNoTransact(studentId, slotId, apptInfo.getTypeKey(), apptInfo, contextInfo);
 580  0
         }
 581  
         // Set number of students allocated
 582  0
         statusInfo.setMessage("" + studentIds.size());
 583  0
     }
 584  
 
 585  
     private AppointmentInfo _createAppointmentInfo(String studentId, String slotId) {
 586  0
         AppointmentInfo apptInfo = new AppointmentInfo();
 587  0
         apptInfo.setPersonId(studentId);
 588  0
         apptInfo.setSlotId(slotId);
 589  0
         apptInfo.setTypeKey(AppointmentServiceConstants.APPOINTMENT_TYPE_REGISTRATION);
 590  0
         apptInfo.setStateKey(AppointmentServiceConstants.APPOINTMENT_STATE_ACTIVE_KEY);
 591  0
         return apptInfo;
 592  
     }
 593  
 
 594  
     // Used by both max/uniform allocation
 595  
     private void _allocateStudentsToSlotsCommon(int studentsPerSlot, List<AppointmentSlotInfo> slotInfoList,
 596  
                                                 List<String> studentIds, ContextInfo contextInfo)
 597  
             throws InvalidParameterException, DataValidationErrorException, MissingParameterException,
 598  
             DoesNotExistException, ReadOnlyException, PermissionDeniedException, OperationFailedException {
 599  0
         int count = 0;
 600  0
         int numStudents = studentIds.size();
 601  0
         int numSlots = slotInfoList.size();
 602  0
         boolean done = false;
 603  0
         for (AppointmentSlotInfo slotInfo: slotInfoList) {
 604  0
             List<String> sublist = null;
 605  
 
 606  
             // Grab sublist of students
 607  0
             int endIndex = (count + 1) * studentsPerSlot;
 608  0
             if (endIndex > numStudents) { // adjust end if we go past number of students
 609  0
                 endIndex = numStudents;
 610  0
                 done = true;
 611  
             }
 612  0
             sublist = studentIds.subList(count * studentsPerSlot, endIndex);
 613  
 
 614  0
             count++;
 615  
             // Make the assignments
 616  0
             for (String studentId: sublist) {
 617  0
                 String slotId = slotInfo.getId();
 618  0
                 AppointmentInfo apptInfo = _createAppointmentInfo(studentId, slotId);
 619  0
                 createAppointmentNoTransact(studentId, slotId, apptInfo.getTypeKey(), apptInfo, contextInfo);
 620  0
             }
 621  0
             if (done) {
 622  0
                 break; // Some slots may be unassigned in the max allocation
 623  
             }
 624  0
         }
 625  0
     }
 626  
     /*
 627  
      * Precondition: At least one slot in the slotInfoList
 628  
      */
 629  
     private void _generateAppointmentsUniformCase(List<String> studentIds, List<AppointmentSlotInfo> slotInfoList,
 630  
                                                   StatusInfo statusInfo, ContextInfo contextInfo) throws InvalidParameterException,
 631  
             DataValidationErrorException, MissingParameterException, DoesNotExistException, ReadOnlyException,
 632  
             PermissionDeniedException, OperationFailedException {
 633  0
         int numSlots = slotInfoList.size();
 634  0
         int numStudents = studentIds.size();
 635  0
         int studentsPerSlot = numStudents / numSlots;
 636  0
         if (numStudents % numSlots != 0) {
 637  0
             studentsPerSlot++; // Add 1 if numSlots does not evenly divide numStudents
 638  
         }
 639  
 
 640  0
         _allocateStudentsToSlotsCommon(studentsPerSlot, slotInfoList, studentIds, contextInfo);
 641  0
         statusInfo.setMessage("" + studentIds.size()); // Set to number of appointments
 642  0
     }
 643  
 
 644  
     // The slot generation will take care of the end dates
 645  
     private void _generateAppointmentsMaxCase(String apptWinId,
 646  
                                               List<String> studentIds, int maxSizePerSlot,
 647  
                                               List<AppointmentSlotInfo> slotInfoList,
 648  
                                               StatusInfo statusInfo, ContextInfo contextInfo) throws InvalidParameterException,
 649  
             DataValidationErrorException, MissingParameterException, DoesNotExistException, ReadOnlyException,
 650  
             PermissionDeniedException, OperationFailedException {
 651  0
         int numSlots = slotInfoList.size();
 652  0
         int numStudents = studentIds.size();
 653  
         // Do we have enough slots for all the students?
 654  0
         if (numStudents > numSlots * maxSizePerSlot) {
 655  
             // No, so quit without doing any more
 656  0
             statusInfo.setSuccess(false);
 657  0
             int diff = numStudents - (numSlots * maxSizePerSlot); // Would be unassigned
 658  0
             statusInfo.setMessage("Not enough room for ["+ numStudents +"] appointments. numSlots[" + numSlots+ "] * maxPerSlot[" +maxSizePerSlot + "] = "
 659  
                     + "["+ (numSlots * maxSizePerSlot)+ "] available appointments. Please increase available slots or max per slot.");
 660  0
             return; // And we're outta here
 661  
         }
 662  
         // Enough slots, so start assigning
 663  0
         _allocateStudentsToSlotsCommon(maxSizePerSlot, slotInfoList, studentIds, contextInfo);
 664  
         // Count how many we assigned
 665  0
         long numAppts = countAppointmentsByApptWinId(apptWinId);
 666  0
         statusInfo.setMessage("" + numAppts); // Set to number of appointments
 667  0
     }
 668  
 
 669  
 
 670  
     // ----------------------- MISC private methods -------------------------------
 671  
     private void _setEndDateToDropDate(AppointmentSlotInfo slotInfo) {
 672  
 //        // TODO: Figure out how to do this
 673  
 //        List<AtpInfo> infoList = atpService.getAtpsByMilestone(apptWin.getPeriodMilestoneId(), contextInfo);
 674  
 //        if (infoList.size() != 1) {
 675  
 //            throw new InvalidParameterException("Should only return one ATP");
 676  
 //        }
 677  
 //        AtpInfo info = infoList.get(0);
 678  
 //        List<MilestoneInfo> milestoneList = atpService.getMilestonesForAtp(info.getId(), contextInfo);
 679  
 //        boolean found = false;
 680  
 //        for (MilestoneInfo milestone: milestoneList) {
 681  
 //            if (milestone.getTypeKey().equals(AtpServiceConstants.MILESTONE_DROP_DATE_TYPE_KEY)) {
 682  
 //                slotInfo.setEndDate(milestone.getEndDate());
 683  
 //                found = true;
 684  
 //                break;
 685  
 //            }
 686  
 //        }
 687  
 //        if (!found) {
 688  
 //            throw new OperationFailedException("Drop date milestone not found");
 689  
 //        }
 690  0
     }
 691  
 }