SlotSafeService.java
/*
* Copyright (c) 2002-2022, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.plugins.appointment.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.collections.CollectionUtils;
import fr.paris.lutece.plugins.appointment.business.appointment.Appointment;
import fr.paris.lutece.plugins.appointment.business.appointment.AppointmentSlot;
import fr.paris.lutece.plugins.appointment.business.form.Form;
import fr.paris.lutece.plugins.appointment.business.planning.TimeSlot;
import fr.paris.lutece.plugins.appointment.business.planning.WeekDefinition;
import fr.paris.lutece.plugins.appointment.business.planning.WorkingDay;
import fr.paris.lutece.plugins.appointment.business.rule.ReservationRule;
import fr.paris.lutece.plugins.appointment.business.slot.Period;
import fr.paris.lutece.plugins.appointment.business.slot.Slot;
import fr.paris.lutece.plugins.appointment.business.slot.SlotHome;
import fr.paris.lutece.plugins.appointment.business.user.User;
import fr.paris.lutece.plugins.appointment.exception.AppointmentSavedException;
import fr.paris.lutece.plugins.appointment.exception.SlotEditTaskExpiredTimeException;
import fr.paris.lutece.plugins.appointment.exception.SlotFullException;
import fr.paris.lutece.plugins.appointment.service.listeners.AppointmentListenerManager;
import fr.paris.lutece.plugins.appointment.service.listeners.SlotListenerManager;
import fr.paris.lutece.plugins.appointment.service.lock.SlotEditTask;
import fr.paris.lutece.plugins.appointment.web.dto.AppointmentDTO;
import fr.paris.lutece.plugins.genericattributes.business.Response;
import fr.paris.lutece.plugins.genericattributes.business.ResponseHome;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.service.admin.AdminUserService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.workflow.WorkflowService;
import fr.paris.lutece.portal.web.l10n.LocaleService;
import fr.paris.lutece.util.sql.TransactionManager;
public final class SlotSafeService
{
private static final ConcurrentMap<Integer, Lock> _listSlot = new ConcurrentHashMap<>( );
private static final ConcurrentMap<Integer, Object> _lockFormId = new ConcurrentHashMap<>( );
/**
* Private constructor - this class does not need to be instantiated
*/
private SlotSafeService( )
{
}
/**
* Get the slot in memory
*
* @return Map of slot
*/
public static Map<Integer, Lock> getListSlotInMemory( )
{
return _listSlot;
}
/**
* get lock for slot
*
* @param nIdSlot
* the Id Slot
* @return return the lock
*/
public static Lock getLockOnSlot( int nIdSlot )
{
if ( nIdSlot == 0 )
{
return new ReentrantLock( );
}
_listSlot.putIfAbsent( nIdSlot, new ReentrantLock( ) );
return _listSlot.get( nIdSlot );
}
/**
* remove slot in map memory
*
* @param nIdSlot
* the Id Slot
*/
public static void removeSlotInMemory( int nIdSlot )
{
_listSlot.remove( nIdSlot );
}
/**
* get lock for form
*
* @param nIdform
* Id from
* @return return lock
*/
private static Object getLockOnForm( int nIdform )
{
_lockFormId.putIfAbsent( nIdform, new Object( ) );
return _lockFormId.get( nIdform );
}
/**
* Create slot
*
* @param slot
* @return slot
*/
public static Slot createSlot( Slot slot )
{
Object formLock = getLockOnForm( slot.getIdForm( ) );
synchronized( formLock )
{
Slot slotSaved = null;
HashMap<LocalDateTime, Slot> slotInDbMap = SlotService.buildMapSlotsByIdFormAndDateRangeWithDateForKey( slot.getIdForm( ),
slot.getStartingDateTime( ), slot.getEndingDateTime( ) );
if ( !slotInDbMap.isEmpty( ) )
{
slotSaved = slotInDbMap.get( slot.getStartingDateTime( ) );
}
else
{
slotSaved = SlotHome.create( slot );
SlotListenerManager.notifyListenersSlotCreation( slot.getIdSlot( ) );
}
return slotSaved;
}
}
/**
*
* Increment max capacity
*
* @param nIdForm
* the Id form
* @param nIncrementingValue
* the incrementing value
* @param startindDateTime
* the starting date Time
* @param endingDateTime
* the ending Date time
* @param lace
* the lace
*/
public static void incrementMaxCapacity( int nIdForm, int nIncrementingValue, LocalDateTime startindDateTime, LocalDateTime endingDateTime, boolean lace )
{
int index = 0;
List<WeekDefinition> listWeekDefinition = WeekDefinitionService.findListWeekDefinition( nIdForm );
Map<WeekDefinition, ReservationRule> mapReservationRule = ReservationRuleService.findAllReservationRule( nIdForm, listWeekDefinition );
List<Slot> listSlot = SlotService.buildListSlot( nIdForm, mapReservationRule, startindDateTime.toLocalDate( ), endingDateTime.toLocalDate( ) );
listSlot = listSlot.stream( )
.filter( slt -> slt.getEndingDateTime( ).isBefore( endingDateTime ) && slt.getEndingDateTime( ).isAfter( startindDateTime ) )
.collect( Collectors.toList( ) );
for ( Slot slot : listSlot )
{
if ( !lace )
{
incrementMaxCapacity( nIncrementingValue, slot );
}
else
{
if ( index % 2 == 0 )
{
incrementMaxCapacity( nIncrementingValue, slot );
}
index++;
}
}
}
/**
* Incrementing max capacity
*
* @param nIncrementingValue
* the incrementing value
* @param slot
* the slot
*/
private static void incrementMaxCapacity( int nIncrementingValue, Slot slot )
{
Slot editSlot = null;
if ( slot.getIdSlot( ) == 0 )
{
editSlot = createSlot( slot );
}
else
{
editSlot = slot;
}
Lock lock = getLockOnSlot( editSlot.getIdSlot( ) );
lock.lock( );
try
{
editSlot = SlotService.findSlotById( editSlot.getIdSlot( ) );
editSlot.setMaxCapacity( editSlot.getMaxCapacity( ) + nIncrementingValue );
editSlot.setNbPotentialRemainingPlaces( editSlot.getNbPotentialRemainingPlaces( ) + nIncrementingValue );
editSlot.setNbRemainingPlaces( editSlot.getNbRemainingPlaces( ) + nIncrementingValue );
editSlot.setIsSpecific( SlotService.isSpecificSlot( editSlot ) );
saveSlot( editSlot );
}
finally
{
lock.unlock( );
}
}
/**
* Update potential remaining places
*
* @param task
* the task timer
*/
public static Slot incrementPotentialRemainingPlaces( SlotEditTask task )
{
Lock lock = getLockOnSlot( task.getIdSlot( ) );
Slot slot;
lock.lock( );
try
{
slot = SlotService.findSlotById( task.getIdSlot( ) );
if ( slot != null )
{
int nNewPotentialRemainingPlaces = Math.min( slot.getNbPotentialRemainingPlaces( ) + task.getNbPlacesTaken( ), slot.getNbRemainingPlaces( ) );
slot.setNbPotentialRemainingPlaces( nNewPotentialRemainingPlaces );
SlotHome.updatePotentialRemainingPlaces( nNewPotentialRemainingPlaces, slot.getIdSlot( ) );
SlotListenerManager.notifyListenersSlotChange( slot.getIdSlot( ) );
}
}
finally
{
lock.unlock( );
}
return slot;
}
/**
* Update potential remaining places
*
* @param nbPotentialRemainingPlaces
* the nbPotentialRemainingPlaces
* @param nIdSlot
* the is Slot
*/
public static void decrementPotentialRemainingPlaces( int nbPotentialRemainingPlaces, int nIdSlot )
{
Lock lock = getLockOnSlot( nIdSlot );
lock.lock( );
try
{
Slot slot = SlotService.findSlotById( nIdSlot );
if ( slot != null )
{
int nNewPotentialRemainingPlaces = slot.getNbPotentialRemainingPlaces( ) - nbPotentialRemainingPlaces;
slot.setNbPotentialRemainingPlaces( nNewPotentialRemainingPlaces );
SlotHome.updatePotentialRemainingPlaces( nNewPotentialRemainingPlaces, nIdSlot );
SlotListenerManager.notifyListenersSlotChange( slot.getIdSlot( ) );
}
}
finally
{
lock.unlock( );
}
}
/**
* Save a slot in database
*
* @param slot
* the slot to save
* @return the slot saved
*/
public static int saveAppointment( AppointmentDTO appointmentDTO, HttpServletRequest request )
{
Locale locale = null;
User user = appointmentDTO.getUser( );
List<Lock> listLock = new ArrayList<>( );
// change date appointment
boolean isReport = appointmentDTO.getIdAppointment( ) != 0;
if ( appointmentDTO.getIsSaved( ) )
{
throw new AppointmentSavedException( "Appointment is already saved " );
}
if ( request != null )
{
locale = LocaleService.getContextUserLocale( request );
for ( Slot slt : appointmentDTO.getSlot( ) )
{
if ( AppointmentUtilities.isEditSlotTaskExpiredTime( request, slt.getIdSlot( ) ) )
throw new SlotEditTaskExpiredTimeException( "appointment edit expired time" );
}
}
AppointmentService.buildListAppointmentSlot( appointmentDTO );
TransactionManager.beginTransaction( AppointmentPlugin.getPlugin( ) );
try
{
Set<Integer> listSlotUpdated = saveSlots( appointmentDTO, listLock, request );
if ( !isReport )
{
user = UserService.saveUser( appointmentDTO );
}
// Create or update the appointment
Appointment appointment = AppointmentService.buildAndCreateAppointment( appointmentDTO, user );
if ( !isReport && CollectionUtils.isNotEmpty( appointmentDTO.getListResponse( ) ) )
{
for ( Response response : appointmentDTO.getListResponse( ) )
{
ResponseHome.create( response );
AppointmentResponseService.insertAppointmentResponse( appointment.getIdAppointment( ), response.getIdResponse( ) );
}
}
processeActionWorkflow( appointment, request, locale, appointmentDTO.getIdForm( ), isReport );
TransactionManager.commitTransaction( AppointmentPlugin.getPlugin( ) );
appointmentDTO.setIdAppointment( appointment.getIdAppointment( ) );
appointmentDTO.setIsSaved( true );
notifyListner( appointment, listSlotUpdated, isReport, locale );
if ( request != null )
{
for ( AppointmentSlot apptSlot : appointmentDTO.getListAppointmentSlot( ) )
{
AppointmentUtilities.cancelTaskTimer( request, apptSlot.getIdSlot( ) );
}
}
appointmentDTO.setReference(appointment.getReference( ));
appointmentDTO.setUser( user );
return appointment.getIdAppointment( );
}
catch( Exception e )
{
TransactionManager.rollBack( AppointmentPlugin.getPlugin( ) );
AppLogService.error( "Error Save appointment " + e.getMessage( ), e );
throw new SlotFullException( e.getMessage( ), e );
}
finally
{
for ( Lock lock : listLock )
{
lock.unlock( );
}
}
}
/**
* notify Appointment/Slot Listner
*
* @param appointment
* the appointment
* @param listSlotUpdated
* the list slot updated to notify
* @param isReport,
* true if it is a postponement of appointment
* @param locale
* the locale
*/
private static void notifyListner( Appointment appointment, Set<Integer> listSlotUpdated, boolean isReport, Locale locale )
{
for ( int idSlot : listSlotUpdated )
{
SlotListenerManager.notifyListenersSlotChange( idSlot );
}
if ( isReport )
{
AppointmentListenerManager.notifyListenersAppointmentDateChanged( appointment.getIdAppointment( ),
appointment.getListAppointmentSlot( ).stream( ).map( AppointmentSlot::getIdSlot ).collect( Collectors.toList( ) ), locale );
if ( appointment.getIdActionReported( ) != 0 )
{
AppointmentListenerManager.notifyAppointmentWFActionTriggered( appointment.getIdAppointment( ), appointment.getIdActionReported( ) );
}
}
else
{
AppointmentListenerManager.notifyListenersAppointmentCreated( appointment.getIdAppointment( ) );
}
}
/**
* Process Action workflow
*
* @param appointment
* the appointment
* @param request
* the request
* @param locale
* the locale
* @param nIdFom
* the id appointment form
* @param isReport
* true if it is a postponement of appointment
*/
private static void processeActionWorkflow( Appointment appointment, HttpServletRequest request, Locale locale, int nIdFom, boolean isReport )
{
Form form = FormService.findFormLightByPrimaryKey( nIdFom );
if ( form.getIdWorkflow( ) > 0 )
{
WorkflowService.getInstance( ).getState( appointment.getIdAppointment( ), Appointment.APPOINTMENT_RESOURCE_TYPE, form.getIdWorkflow( ),
form.getIdForm( ) );
if ( isReport && appointment.getIdActionReported( ) != 0 )
{
AdminUser adminUser = ( request != null ) ? AdminUserService.getAdminUser( request ) : null;
WorkflowService.getInstance( ).doProcessAction( appointment.getIdAppointment( ), Appointment.APPOINTMENT_RESOURCE_TYPE,
appointment.getIdActionReported( ), form.getIdForm( ), request, locale, adminUser == null, adminUser );
}
}
}
/**
* Set the new number of remaining places (and potential) when an appointment is deleted or cancelled This new value must take in account the capacity of
* the slot, in case of the slot was already over booked
*
* @param nbPlaces
* the nb places taken of the appointment that we want to delete (or cancel, or move)
* @param slot
* the related slot
*/
static void updateRemaningPlacesWithAppointmentMovedDeletedOrCanceled( int nbPlaces, int nIdSlot )
{
// The capacity of the slot (that can be less than the number of places
// taken on the slot --> overbook)
Lock lock = getLockOnSlot( nIdSlot );
lock.lock( );
try
{
Slot slot = SlotService.findSlotById( nIdSlot );
if ( slot != null )
{
int nMaxCapacity = slot.getMaxCapacity( );
// The old remaining places of the slot (before we delete or cancel or move the
// appointment
int nOldRemainingPlaces = slot.getNbRemainingPlaces( );
int nOldPotentialRemaningPlaces = slot.getNbPotentialRemainingPlaces( );
int nOldPlacesTaken = slot.getNbPlacesTaken( );
int nNewPlacesTaken = nOldPlacesTaken - nbPlaces;
// The new value of the remaining places of the slot is the minimal
// value between :
// - the minimal value between the potentially new max capacity and the old remaining places plus the number of places released by the
// appointment
// - and the capacity of the slot minus the new places taken on the slot
int nNewRemainingPlaces = Math.min( Math.min( nMaxCapacity, nOldRemainingPlaces + nbPlaces ), ( nMaxCapacity - nNewPlacesTaken ) );
int nNewPotentialRemainingPlaces = Math.min( Math.min( nMaxCapacity, nOldPotentialRemaningPlaces + nbPlaces ),
( nMaxCapacity - nNewPlacesTaken ) );
slot.setNbRemainingPlaces( nNewRemainingPlaces );
slot.setNbPotentialRemainingPlaces( nNewPotentialRemainingPlaces );
slot.setNbPlacestaken( nNewPlacesTaken );
SlotHome.update( slot );
}
}
finally
{
lock.unlock( );
}
}
/**
* Set the new number of remaining places (and potential) when an appointment is reactivated(not reserved to reserved) This new value must take in account
* the capacity of the slot, in case of the slot was already over booked
*
* @param nbPlaces
* the nb places taken of the appointment on the slot
* @param nIdSlot
* the id slot to update
*/
static void updateRemaningPlacesWithAppointmentReactivated( int nbPlaces, int nIdSlot )
{
// The capacity of the slot (that can be less than the number of places
// taken on the slot --> overbook)
Lock lock = getLockOnSlot( nIdSlot );
lock.lock( );
try
{
Slot slot = SlotService.findSlotById( nIdSlot );
if ( slot != null )
{
slot.setNbRemainingPlaces( slot.getNbRemainingPlaces( ) - nbPlaces );
slot.setNbPotentialRemainingPlaces( slot.getNbPotentialRemainingPlaces( ) - nbPlaces );
slot.setNbPlacestaken( slot.getNbPlacesTaken( ) + nbPlaces );
SlotHome.update( slot );
}
}
finally
{
lock.unlock( );
}
}
/**
* Update a slot in database and possibly all the slots after (if the ending hour has changed, all the next slots are impacted in case of the user decide to
* shift the next slots)
*
* @param slot
* the slot to update
* @param bEndingTimeHasChanged
* true if the ending time has changed
* @param previousEndingTime
* the previous ending time
* @param bShifSlot
* true if the user has decided to shift the next slots
*/
public static void updateSlot( Slot slot, boolean bEndingTimeHasChanged, LocalTime previousEndingTime, boolean bShifSlot )
{
slot.setIsSpecific( SlotService.isSpecificSlot( slot ) );
if ( bEndingTimeHasChanged )
{
// If we don't want to shift the next slots
if ( !bShifSlot )
{
updateSlotWithoutShift( slot );
}
else
{
// We want to shift the next slots at the end of the current
// slot
updateSlotWithShift( slot, previousEndingTime );
}
SlotListenerManager.notifySlotEndingTimeHasChanged( slot.getIdSlot( ), slot.getIdForm( ), slot.getEndingDateTime( ) );
}
else
{
// The ending time of the slot has not changed
// If it's an update of an existing slot
if ( slot.getIdSlot( ) != 0 )
{
updateRemainingPlaces( slot );
}
saveSlot( slot );
}
}
/**
* Update the current slot and don't shift the next slots
*
* @param slot
* the current slot
*/
private static void updateSlotWithoutShift( Slot slot )
{
List<Slot> listSlotToCreate = new ArrayList<>( );
// Need to get all the slots until the new end of this slot
List<Slot> listSlotToDelete = SlotService.findSlotsByIdFormAndDateRange( slot.getIdForm( ), slot.getStartingDateTime( ).plusMinutes( 1 ),
slot.getEndingDateTime( ) );
SlotService.deleteListSlots( listSlotToDelete );
// Get the list of slot after the modified slot
HashMap<LocalDateTime, Slot> mapNextSlot = SlotService.buildMapSlotsByIdFormAndDateRangeWithDateForKey( slot.getIdForm( ), slot.getEndingDateTime( ),
slot.getDate( ).atTime( LocalTime.MAX ) );
List<LocalDateTime> listStartingDateTimeNextSlot = new ArrayList<>( mapNextSlot.keySet( ) );
// Get the next date time slot
LocalDateTime nextStartingDateTime = null;
if ( CollectionUtils.isNotEmpty( listStartingDateTimeNextSlot ) )
{
nextStartingDateTime = Utilities.getClosestDateTimeInFuture( listStartingDateTimeNextSlot, slot.getEndingDateTime( ) );
}
else
{
LocalDate dateOfSlot = slot.getDate( );
ReservationRule reservationRule = ReservationRuleService.findReservationRuleByIdFormAndClosestToDateOfApply( slot.getIdForm( ), dateOfSlot );
WorkingDay workingDay = WorkingDayService.getWorkingDayOfDayOfWeek( reservationRule.getListWorkingDay( ), dateOfSlot.getDayOfWeek( ) );
// No slot after this one.
// Need to compute between the end of this slot and the next
// time slot
if ( workingDay != null )
{
List<TimeSlot> nextTimeSlots = TimeSlotService.getNextTimeSlotsInAListOfTimeSlotAfterALocalTime( workingDay.getListTimeSlot( ),
slot.getEndingTime( ) );
if ( CollectionUtils.isNotEmpty( nextTimeSlots ) )
{
Optional<TimeSlot> optTimeSlot = nextTimeSlots.stream( ).min( ( t1, t2 ) -> t1.getStartingTime( ).compareTo( t2.getStartingTime( ) ) );
if ( optTimeSlot.isPresent( ) )
{
nextStartingDateTime = optTimeSlot.get( ).getStartingTime( ).atDate( dateOfSlot );
}
}
}
else
{
// This is not a working day
// Generated the new slots at the end of the modified
// slot
listSlotToCreate.addAll( generateListSlotToCreateAfterATime( slot.getEndingDateTime( ), slot.getIdForm( ) ) );
}
}
// Need to create a slot between these two dateTime
if ( nextStartingDateTime != null && !slot.getEndingDateTime( ).isEqual( nextStartingDateTime ) )
{
Slot slotToCreate = SlotService.buildSlot( slot.getIdForm( ), new Period( slot.getEndingDateTime( ), nextStartingDateTime ), slot.getMaxCapacity( ),
slot.getMaxCapacity( ), slot.getMaxCapacity( ), 0, Boolean.FALSE, Boolean.TRUE );
listSlotToCreate.add( slotToCreate );
}
// If it's an update of an existing slot
if ( slot.getIdSlot( ) != 0 )
{
updateRemainingPlaces( slot );
}
saveSlot( slot );
createListSlot( listSlotToCreate );
}
/**
* update the current slot and shift the next slots at the end of the current slot
*
* @param slot
* the current slot
* @param previousEndingTime
* the previous ending time of the current slot
*/
private static void updateSlotWithShift( Slot slot, LocalTime previousEndingTime )
{
// We want to shift all the next slots
LocalDate dateOfSlot = slot.getDate( );
List<WeekDefinition> listWeekDefinition = WeekDefinitionService.findListWeekDefinition( slot.getIdForm( ) );
Map<WeekDefinition, ReservationRule> mapReservationRule = ReservationRuleService.findAllReservationRule( slot.getIdForm( ), listWeekDefinition );
// Build or get all the slots of the day
List<Slot> listAllSlotsOfThisDayToBuildOrInDb = SlotService.buildListSlot( slot.getIdForm( ), mapReservationRule, dateOfSlot, dateOfSlot );
// Remove the current slot and all the slot before it
listAllSlotsOfThisDayToBuildOrInDb = listAllSlotsOfThisDayToBuildOrInDb.stream( )
.filter( slotToKeep -> slotToKeep.getStartingDateTime( ).isAfter( slot.getStartingDateTime( ) ) ).collect( Collectors.toList( ) );
// Need to delete all the slots until the new end of this slot
List<Slot> listSlotToDelete = listAllSlotsOfThisDayToBuildOrInDb.stream( )
.filter( slotToDelete -> slotToDelete.getStartingDateTime( ).isAfter( slot.getStartingDateTime( ) )
&& !slotToDelete.getEndingDateTime( ).isAfter( slot.getEndingDateTime( ) ) && slotToDelete.getIdSlot( ) != 0 )
.collect( Collectors.toList( ) );
SlotService.deleteListSlots( listSlotToDelete );
listAllSlotsOfThisDayToBuildOrInDb.removeAll( listSlotToDelete );
// Need to find all the existing slots
List<Slot> listExistingSlots = listAllSlotsOfThisDayToBuildOrInDb.stream( ).filter( existingSlot -> existingSlot.getIdSlot( ) != 0 )
.collect( Collectors.toList( ) );
// Remove them from the list of slot to build
listAllSlotsOfThisDayToBuildOrInDb.removeAll( listExistingSlots );
// Save this list
createListSlot( listAllSlotsOfThisDayToBuildOrInDb );
List<Slot> listSlotToShift = new ArrayList<>( );
listSlotToShift.addAll( listExistingSlots );
listSlotToShift.addAll( listAllSlotsOfThisDayToBuildOrInDb );
// Need to order the list of slot to shift according to the shift
// if the new ending time is before the previous ending time,
// the list has to be ordered in chronological order ascending
// and the first slot to shift is the closest to the current
// slot
// (because we have an integrity constraint for the slot, it
// can't have the same starting or ending time as another slot
listSlotToShift = listSlotToShift.stream( ).sorted( ( slot1, slot2 ) -> slot1.getStartingDateTime( ).compareTo( slot2.getStartingDateTime( ) ) )
.collect( Collectors.toList( ) );
boolean bNewEndingTimeIsAfterThePreviousTime = false;
// Need to know the ending time of the day
LocalDateTime endingDateTimeOfTheDay = null;
ReservationRule reservationRule = ReservationRuleService.findReservationRuleByIdFormAndClosestToDateOfApply( slot.getIdForm( ), dateOfSlot );
WorkingDay workingDay = WorkingDayService.getWorkingDayOfDayOfWeek( reservationRule.getListWorkingDay( ), dateOfSlot.getDayOfWeek( ) );
LocalTime endingTimeOfTheDay;
if ( workingDay != null )
{
endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
}
else
{
endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAListOfWorkingDay( reservationRule.getListWorkingDay( ) );
}
endingDateTimeOfTheDay = endingTimeOfTheDay.atDate( dateOfSlot );
long timeToAdd = 0;
long timeToSubstract = 0;
if ( previousEndingTime.isBefore( slot.getEndingTime( ) ) )
{
bNewEndingTimeIsAfterThePreviousTime = true;
// Need to find the next available slot, to know how to
// add to the starting time of the next slot to match
// with the new end of the current slot
if ( CollectionUtils.isNotEmpty( listSlotToShift ) )
{
Slot nextSlot = listSlotToShift.stream( ).min( ( s1, s2 ) -> s1.getStartingDateTime( ).compareTo( s2.getStartingDateTime( ) ) ).orElse( slot );
if ( slot.getEndingDateTime( ).isAfter( nextSlot.getStartingDateTime( ) ) )
{
timeToAdd = nextSlot.getStartingDateTime( ).until( slot.getEndingDateTime( ), ChronoUnit.MINUTES );
}
else
{
timeToAdd = slot.getEndingDateTime( ).until( nextSlot.getStartingDateTime( ), ChronoUnit.MINUTES );
}
Collections.reverse( listSlotToShift );
}
else
{
timeToAdd = previousEndingTime.until( slot.getEndingTime( ), ChronoUnit.MINUTES );
}
}
else
{
timeToSubstract = slot.getEndingTime( ).until( previousEndingTime, ChronoUnit.MINUTES );
}
// If it's an update of an existing slot
if ( slot.getIdSlot( ) != 0 )
{
updateRemainingPlaces( slot );
}
saveSlot( slot );
// Need to set the new starting and ending time of all the slots
// to shift and update them
for ( Slot slotToShift : listSlotToShift )
{
// If the new ending time is after the previous time
if ( bNewEndingTimeIsAfterThePreviousTime )
{
// If the starting time + the time to add is before the
// ending time of the day
if ( slotToShift.getStartingDateTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isBefore( endingDateTimeOfTheDay ) )
{
slotToShift.setStartingDateTime( slotToShift.getStartingDateTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
// if the ending time is after the ending time of
// the day, we set the new ending time to the ending
// time of the day
if ( slotToShift.getEndingDateTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isAfter( endingDateTimeOfTheDay ) )
{
slotToShift.setEndingDateTime( endingDateTimeOfTheDay );
}
else
{
slotToShift.setEndingDateTime( slotToShift.getEndingDateTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
}
slotToShift.setIsSpecific( SlotService.isSpecificSlot( slotToShift ) );
saveSlot( slotToShift );
}
else
{
// Delete this slot (the slot can not be after the
// ending time of the day)
SlotService.deleteSlot( slotToShift );
}
}
else
{
// The new ending time is before the previous ending
// time
slotToShift.setStartingDateTime( slotToShift.getStartingDateTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
slotToShift.setEndingDateTime( slotToShift.getEndingDateTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
slotToShift.setIsSpecific( SlotService.isSpecificSlot( slotToShift ) );
saveSlot( slotToShift );
}
}
if ( !bNewEndingTimeIsAfterThePreviousTime )
{
// If the slots have been shift earlier,
// there is no slot(s) between the last slot created
// and the ending time of the day, need to create it(them)
List<Slot> listSlotsToAdd = generateListSlotToCreateAfterATime( endingDateTimeOfTheDay.minusMinutes( timeToSubstract ), slot.getIdForm( ) );
createListSlot( listSlotsToAdd );
}
}
/**
* Generate the list of slot to create after a slot (taking into account the week definition and the rules to apply)
*
* @param slot
* the slot
* @return the list of next slots
*/
private static List<Slot> generateListSlotToCreateAfterATime( LocalDateTime dateTimeToStartCreation, int nIdForm )
{
List<Slot> listSlotToCreate = new ArrayList<>( );
LocalDate dateOfCreation = dateTimeToStartCreation.toLocalDate( );
ReservationRule reservationRule = ReservationRuleService.findReservationRuleByIdFormAndClosestToDateOfApply( nIdForm, dateOfCreation );
int nMaxCapacity = reservationRule.getMaxCapacityPerSlot( );
WorkingDay workingDay = WorkingDayService.getWorkingDayOfDayOfWeek( reservationRule.getListWorkingDay( ), dateOfCreation.getDayOfWeek( ) );
LocalTime endingTimeOfTheDay = null;
List<TimeSlot> listTimeSlot = new ArrayList<>( );
int nDurationSlot = reservationRule.getDurationAppointments( );
if ( workingDay != null )
{
endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
listTimeSlot = TimeSlotService.findListTimeSlotByWorkingDay( workingDay.getIdWorkingDay( ) );
}
else
{
endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAListOfWorkingDay( reservationRule.getListWorkingDay( ) );
}
LocalDateTime endingDateTimeOfTheDay = endingTimeOfTheDay.atDate( dateOfCreation );
LocalDateTime startingDateTime = dateTimeToStartCreation;
LocalDateTime endingDateTime = startingDateTime.plusMinutes( nDurationSlot );
while ( !endingDateTime.isAfter( endingDateTimeOfTheDay ) )
{
Slot slotToCreate = SlotService.buildSlot( nIdForm, new Period( startingDateTime, endingDateTime ), nMaxCapacity, nMaxCapacity, nMaxCapacity, 0,
Boolean.FALSE, Boolean.TRUE );
slotToCreate.setIsSpecific( SlotService.isSpecificSlot( slotToCreate, workingDay, listTimeSlot, nMaxCapacity ) );
startingDateTime = endingDateTime;
endingDateTime = startingDateTime.plusMinutes( nDurationSlot );
listSlotToCreate.add( slotToCreate );
}
if ( startingDateTime.isBefore( endingDateTimeOfTheDay ) && endingDateTime.isAfter( endingDateTimeOfTheDay ) )
{
Slot slotToCreate = SlotService.buildSlot( nIdForm, new Period( startingDateTime, endingDateTimeOfTheDay ), nMaxCapacity, nMaxCapacity,
nMaxCapacity, 0, Boolean.FALSE, Boolean.TRUE );
slotToCreate.setIsSpecific( SlotService.isSpecificSlot( slotToCreate, workingDay, listTimeSlot, nMaxCapacity ) );
listSlotToCreate.add( slotToCreate );
}
return listSlotToCreate;
}
/**
* Build a Slot object from the resultset
*
* @param daoUtil
* the prepare statement util object
*
*/
/**
* Update the capacity of the slot
*
* @param slot
* the slot to update
*/
public static void updateRemainingPlaces( Slot slot )
{
Slot oldSlot = SlotHome.findByPrimaryKey( slot.getIdSlot( ) );
int nNewNbMaxCapacity = slot.getMaxCapacity( );
int nOldBnMaxCapacity = oldSlot.getMaxCapacity( );
// If the max capacity has been modified
if ( nNewNbMaxCapacity != nOldBnMaxCapacity )
{
// Need to update the remaining places
// Need to add the diff between the old value and the new value
// to the remaining places (if the new is higher)
if ( nNewNbMaxCapacity > nOldBnMaxCapacity )
{
int nValueToAdd = nNewNbMaxCapacity - nOldBnMaxCapacity;
slot.setNbPotentialRemainingPlaces( oldSlot.getNbPotentialRemainingPlaces( ) + nValueToAdd );
slot.setNbRemainingPlaces( oldSlot.getNbRemainingPlaces( ) + nValueToAdd );
}
else
{
// the new value is lower than the previous capacity
// !!!! If there are appointments on this slot and if the
// slot is already full, the slot will be surbooked !!!!
int nValueToSubstract = nOldBnMaxCapacity - nNewNbMaxCapacity;
slot.setNbPotentialRemainingPlaces( oldSlot.getNbPotentialRemainingPlaces( ) - nValueToSubstract );
slot.setNbRemainingPlaces( oldSlot.getNbRemainingPlaces( ) - nValueToSubstract );
}
}
else
{
slot.setNbPotentialRemainingPlaces( oldSlot.getNbPotentialRemainingPlaces( ) );
slot.setNbRemainingPlaces( oldSlot.getNbRemainingPlaces( ) );
}
}
/**
* Save a slot in database
*
* @param slot
* the slot to save
* @return the slot saved
*/
public static Slot saveSlot( Slot slot )
{
Slot slotSaved = null;
if ( slot.getIdSlot( ) == 0 )
{
slotSaved = createSlot( slot );
}
else
{
slotSaved = updateSlot( slot );
}
return slotSaved;
}
/**
* Update a slot
*
* @param slot
* the slot updated
*/
public static Slot updateSlot( Slot slot )
{
Slot slotToReturn = SlotHome.update( slot );
SlotListenerManager.notifyListenersSlotChange( slot.getIdSlot( ) );
return slotToReturn;
}
/**
* Create in database the slots given
*
* @param listSlotToCreate
* the list of slots to create in database
*/
private static void createListSlot( List<Slot> listSlotToCreate )
{
if ( CollectionUtils.isNotEmpty( listSlotToCreate ) )
{
for ( Slot slotTemp : listSlotToCreate )
{
createSlot( slotTemp );
}
}
}
/**
* Clean slotlist
*/
public static void cleanSlotlist( )
{
Iterator<Integer> it = _listSlot.keySet( ).iterator( );
int idSlot;
while ( it.hasNext( ) )
{
idSlot = it.next( );
Slot slot = SlotService.findSlotById( idSlot );
if ( slot == null || slot.getStartingDateTime( ).isBefore( LocalDateTime.now( ) ) || slot.getMaxCapacity( ) <= slot.getNbPlacesTaken( ) )
{
_listSlot.remove( idSlot );
}
}
}
/**
* Save and update slots
*
* @param appointmentDTO
* the appointmentDTO
* @return list id slot updated
* @throws InterruptedException
*/
private static Set<Integer> saveSlots( AppointmentDTO appointmentDTO, List<Lock> listLock, HttpServletRequest request ) throws InterruptedException, CloneNotSupportedException
{
Appointment oldAppointment = null;
List<Slot> listOldSlot = new ArrayList<>( );
List<Slot> listSlotToUpdate = new ArrayList<>( );
int nbSumRemainingPlaces = 0;
// if it's an update for modification of the date of the appointment
if ( appointmentDTO.getIdAppointment( ) != 0 )
{
oldAppointment = AppointmentService.findAppointmentById( appointmentDTO.getIdAppointment( ) );
if ( oldAppointment.getIsCancelled( ) )
{
throw new SlotFullException( "ERROR APPOINTMENT CANCELLED " );
}
// Need to update the old slot
for ( AppointmentSlot appointmentSlot : oldAppointment.getListAppointmentSlot( ) )
{
Lock lock = getLockOnSlot( appointmentSlot.getIdSlot( ) );
if ( lock.tryLock( 3, TimeUnit.SECONDS ) )
{
listLock.add( lock );
}
else
{
throw new SlotFullException( "ERROR SLOT LOCKED" );
}
Slot slt = SlotService.findSlotById( appointmentSlot.getIdSlot( ) );
oldAppointment.addSlot( slt.clone( ) );
slt = updateRemaningPlacesWithAppointmentMoved( appointmentSlot.getNbPlaces( ), slt );
listOldSlot.add( slt );
}
//We set the appointmentDTO object in the request before proceeding with its update,
//especially in the context of report an appointment.
//This ensures that the object will be available in the request parameter that we pass during the execution of workflow tasks.
request.setAttribute(AppointmentUtilities.OLD_APPOINTMENT_DTO, AppointmentUtilities.buildAppointmentDTO(oldAppointment));
}
for ( AppointmentSlot appSlot : appointmentDTO.getListAppointmentSlot( ) )
{
Slot slt = null;
// if it's an update for modification of the date of the appointment we load the slots in the listOldSlot because are updated already
if ( appointmentDTO.getIdAppointment( ) != 0 && listOldSlot.stream( ).anyMatch( slot -> slot.getIdSlot( ) == appSlot.getIdSlot( ) ) )
{
slt = listOldSlot.stream( ).filter( slot -> slot.getIdSlot( ) == appSlot.getIdSlot( ) ).findFirst( ).orElse( null );
listOldSlot.removeIf( slot -> slot.getIdSlot( ) == appSlot.getIdSlot( ) );
}
else
{
Lock lock = getLockOnSlot( appSlot.getIdSlot( ) );
if ( lock.tryLock( 3, TimeUnit.SECONDS ) )
{
listLock.add( lock );
}
else
{
throw new SlotFullException( "ERROR SLOT LOCKED" );
}
slt = SlotService.findSlotById( appSlot.getIdSlot( ) );
}
if ( slt == null || ( ( appSlot.getNbPlaces( ) > slt.getNbRemainingPlaces( ) && !appointmentDTO.getOverbookingAllowed( ) )
|| slt.getEndingDateTime( ).isBefore( LocalDateTime.now( ) ) ) )
{
AppLogService.error( "ERROR SLOT FULL, ID SLOT: " + appSlot.getIdSlot( ) );
throw new SlotFullException( "ERROR SLOT FULL " );
}
nbSumRemainingPlaces = nbSumRemainingPlaces + slt.getNbRemainingPlaces( );
// Update of the remaining places of the slot and appointmentDTO if over booking Allowed
updateRemaningPlacesAndappointmentDTO( appSlot.getNbPlaces( ), slt, appointmentDTO );
listSlotToUpdate.add( slt );
}
// this test is for form with the possibility of taking several appointments on the same slot
if ( appointmentDTO.getNbBookedSeats( ) > nbSumRemainingPlaces && !appointmentDTO.getOverbookingAllowed( ) )
{
AppLogService.error( "ERROR SLOT FULL" );
throw new SlotFullException( "ERROR SLOT FULL" );
}
listSlotToUpdate.addAll( listOldSlot );
return updateListSlots( listSlotToUpdate );
}
/**
* Update slots passed in the parmaters
*
* @param listSlotToUpdate
* the list of slot to update
* @return ids list slot Updated
*/
private static Set<Integer> updateListSlots( List<Slot> listSlotToUpdate )
{
Set<Integer> listSlot = new HashSet<>( );
for ( Slot slot : listSlotToUpdate )
{
SlotHome.update( slot );
listSlot.add( slot.getIdSlot( ) );
}
return listSlot;
}
/**
* Set the new number of remaining places (and potential) when an appointment is moved This new value must take in account the capacity of the slot, in case
* of the slot was already over booked
*
* @param nbPlaces
* the nb places taken of the appointment that we want to move
* @param slot
* the related slot
* @return the slot updated
*/
private static Slot updateRemaningPlacesWithAppointmentMoved( int nbPlaces, Slot slot )
{
// The capacity of the slot (that can be less than the number of places
// taken on the slot --> overbook)
int nMaxCapacity = slot.getMaxCapacity( );
// The old remaining places of the slot before we move the appointment
int nOldRemainingPlaces = slot.getNbRemainingPlaces( );
int nOldPotentialRemaningPlaces = slot.getNbPotentialRemainingPlaces( );
int nOldPlacesTaken = slot.getNbPlacesTaken( );
int nNewPlacesTaken = nOldPlacesTaken - nbPlaces;
// The new value of the remaining places of the slot is the minimal
// value between :
// - the minimal value between the potentially new max capacity and the old remaining places plus the number of places released by the appointment
// - and the capacity of the slot minus the new places taken on the slot
int nNewRemainingPlaces = Math.min( Math.min( nMaxCapacity, nOldRemainingPlaces + nbPlaces ), ( nMaxCapacity - nNewPlacesTaken ) );
int nNewPotentialRemainingPlaces = Math.min( Math.min( nNewRemainingPlaces, nOldPotentialRemaningPlaces + nbPlaces ),
( nMaxCapacity - nNewPlacesTaken ) );
slot.setNbRemainingPlaces( nNewRemainingPlaces );
slot.setNbPotentialRemainingPlaces( nNewPotentialRemainingPlaces );
slot.setNbPlacestaken( nNewPlacesTaken );
return slot;
}
/**
* update remaning places and appointmentDTO
*
* @param effectiveBookedSeats
* the effective booked seats
* @param slt
* the slot
* @param appointmentDTO
* the appointment
*/
private static void updateRemaningPlacesAndappointmentDTO( int effectiveBookedSeats, Slot slt, AppointmentDTO appointmentDTO )
{
// Update of the remaining places of the slot
int newNbRemainingPlaces = slt.getNbRemainingPlaces( ) - effectiveBookedSeats;
int newPotentialRemaningPlaces = slt.getNbPotentialRemainingPlaces( ) + appointmentDTO.getNbMaxPotentialBookedSeats( ) - effectiveBookedSeats;
int newNbPlacesTaken = slt.getNbPlacesTaken( ) + effectiveBookedSeats;
slt.setNbRemainingPlaces( newNbRemainingPlaces );
slt.setNbPlacestaken( newNbPlacesTaken );
slt.setNbPotentialRemainingPlaces( Math.min( newPotentialRemaningPlaces, newNbRemainingPlaces ) );
if ( slt.getNbPlacesTaken( ) > slt.getMaxCapacity( ) )
{
if ( appointmentDTO.getOverbookingAllowed( ) )
{
appointmentDTO.setIsSurbooked( true );
}
else
{
throw new SlotFullException( "case of overbooking" );
}
}
}
}