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