SlotUtil.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.modules.solr.service;

import fr.paris.lutece.plugins.appointment.business.slot.Slot;
import fr.paris.lutece.plugins.appointment.service.SlotService;
import fr.paris.lutece.plugins.appointment.service.WeekDefinitionService;
import fr.paris.lutece.plugins.appointment.web.dto.AppointmentFormDTO;
import fr.paris.lutece.plugins.search.solr.indexer.SolrIndexerService;
import fr.paris.lutece.plugins.search.solr.indexer.SolrItem;
import fr.paris.lutece.portal.web.l10n.LocaleService;
import fr.paris.lutece.util.url.UrlItem;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * Utils for the slots (Uid, Url, Item ...)
 * 
 * @author Laurent Payen
 *
 */
public final class SlotUtil
{

    private static final String DAY_OPEN = "day_open";
    private static final String ENABLED = "enabled";
    private static final String SLOT_NB_FREE_PLACES = "slot_nb_free_places";
    private static final String SLOT_NB_PLACES = "slot_nb_places";
    private static final String DAY_OF_WEEK = "day_of_week";
    private static final String MINUTE_OF_DAY = "minute_of_day";
    private static final String NB_CONSECUTIVES_SLOTS = "nb_consecutives_slots";
    private static final String APPOINTMENT_MULTISLOTS = "appointment_multislots";
    private static final String MAX_CONSECUTIVES_SLOTS = "max_consecutives_slots";
    private static final String UID_FORM = "uid_form";
    private static final String URL_FORM = "url_form";
    private static final String APPOINTMENT_SLOT = "appointmentslot";
    private static final String VIEW_FORM = "getViewAppointmentForm";

    private static final String PARAMETER_STARTING_DATETIME = "starting_date_time";
    private static final String PARAMETER_ANCHOR = "anchor";
    private static final String VALUE_ANCHOR = "step3";

    /**
     * Private constructor - this class does not need to be instantiated
     */
    private SlotUtil( )
    {
    }

    /**
     * Generate a unique ID for solr.
     *
     * Slots don't have ids anymore, so we use the form_id and the slot date as an ID. We try to make a "readable" id with the form id and the slot datetime,
     * using only alphanumerical caracters to avoid potential problems with code parsing this ID.
     * 
     */
    public static String getSlotUid( Slot slot )
    {
        String strSlotDateFormatted = slot.getStartingDateTime( ).format( Utilities.SLOT_SOLR_ID_DATE_FORMATTER );
        return "F" + slot.getIdForm( ) + "D" + strSlotDateFormatted;
    }

    /**
     * Get the slot url to call directly rdv v2 with the good parameters
     * 
     * @param slot
     *            the slot
     * @return the url with all the parameters
     */
    public static String getSlotUrl( Slot slot )
    {
        UrlItem url = new UrlItem( SolrIndexerService.getBaseUrl( ) );
        url.addParameter( Utilities.PARAMETER_XPAGE, Utilities.XPAGE_APPOINTMENT );
        url.addParameter( Utilities.PARAMETER_VIEW, VIEW_FORM );
        url.addParameter( FormUtil.PARAMETER_ID_FORM, slot.getIdForm( ) );
        url.addParameter( PARAMETER_STARTING_DATETIME, slot.getStartingDateTime( ).toString( ) );
        url.addParameter( PARAMETER_ANCHOR, VALUE_ANCHOR );
        return url.getUrl( );
    }

    /**
     * Build and return the slot Item for Solr
     * 
     * @param appointmentForm
     *            the Appointment Form
     * @param slot
     *            the slot
     * @return the slot Item
     */
    public static SolrItem getSlotItem( AppointmentFormDTO appointmentForm, Slot slot, List<Slot> allSlots )
    {
        // the item
        SolrItem item = FormUtil.getDefaultFormItem( appointmentForm );
        item.setUid( Utilities.buildResourceUid( getSlotUid( slot ), Utilities.RESOURCE_TYPE_SLOT ) );
        item.addDynamicFieldNotAnalysed( UID_FORM, FormUtil.getFormUid( appointmentForm.getIdForm( ) ) );
        item.setUrl( getSlotUrl( slot ) );
        item.addDynamicFieldNotAnalysed( URL_FORM, FormUtil.getFormUrl( appointmentForm.getIdForm( ) ) );
        item.setDate( slot.getStartingTimestampDate( ) );
        item.setType( Utilities.SHORT_NAME_SLOT );
        if ( StringUtils.isNotEmpty( appointmentForm.getAddress( ) ) && appointmentForm.getLongitude( ) != null && appointmentForm.getLatitude( ) != null )
        {
            item.addDynamicFieldGeoloc( APPOINTMENT_SLOT, appointmentForm.getAddress( ), appointmentForm.getLongitude( ), appointmentForm.getLatitude( ),
                    "appointmentslot-" + slot.getNbPotentialRemainingPlaces( ) + "/" + slot.getMaxCapacity( ) );
        }
        item.addDynamicFieldNotAnalysed( DAY_OPEN, String.valueOf( Boolean.TRUE ) );
        item.addDynamicFieldNotAnalysed( ENABLED, String.valueOf( slot.getIsOpen( ) ) );
        item.addDynamicField( SLOT_NB_FREE_PLACES, Long.valueOf( slot.getNbPotentialRemainingPlaces( ) ) );
        item.addDynamicField( SLOT_NB_PLACES, Long.valueOf( slot.getMaxCapacity( ) ) );
        item.addDynamicField( DAY_OF_WEEK, Long.valueOf( slot.getStartingDateTime( ).getDayOfWeek( ).getValue( ) ) );
        item.addDynamicField( MINUTE_OF_DAY,
                ChronoUnit.MINUTES.between( slot.getStartingDateTime( ).toLocalDate( ).atStartOfDay( ), slot.getStartingDateTime( ) ) );

        long consecutiveSlots = calculateConsecutiveSlots(slot, allSlots);
        item.addDynamicField( APPOINTMENT_MULTISLOTS, Boolean.toString( appointmentForm.getIsMultislotAppointment( ) ) );
        if (appointmentForm.getIsMultislotAppointment()) {
            item.addDynamicField(MAX_CONSECUTIVES_SLOTS, Long.valueOf(appointmentForm.getNbConsecutiveSlots()));
            if (consecutiveSlots <= appointmentForm.getNbConsecutiveSlots()) {
                item.addDynamicField(NB_CONSECUTIVES_SLOTS, consecutiveSlots);
            }
            else
            {
                item.addDynamicField(NB_CONSECUTIVES_SLOTS, (long) appointmentForm.getNbConsecutiveSlots());
            }
        } else {
            item.addDynamicField(NB_CONSECUTIVES_SLOTS, 1L);
            item.addDynamicField(MAX_CONSECUTIVES_SLOTS, 1L);
        }

        // Date Hierarchy
        item.setHieDate( slot.getStartingDateTime( ).toLocalDate( ).format( Utilities.HIE_DATE_FORMATTER ) );
        return item;
    }

    /**
     * Get all the slots of a form by calling the method buildListSlot of the plugin RDV
     * 
     * @param appointmentForm
     *            the appointment form
     * @return all the slots of a form
     */
    public static List<Slot> getAllSlots( AppointmentFormDTO appointmentForm )
    {
        // Get the nb weeks to display
        int nNbWeeksToDisplay = appointmentForm.getNbWeeksToDisplay( );
        LocalDate startingDateOfDisplay = LocalDate.now( );
        if ( appointmentForm.getDateStartValidity( ) != null && startingDateOfDisplay.isBefore( appointmentForm.getDateStartValidity( ).toLocalDate( ) ) )
        {
            startingDateOfDisplay = appointmentForm.getDateStartValidity( ).toLocalDate( );
        }
        // Calculate the ending date of display with the nb weeks to display
        // since today
        // We calculate the number of weeks including the current week, so it
        // will end to the (n) next sunday
        TemporalField fieldISO = WeekFields.of( LocaleService.getDefault( ) ).dayOfWeek( );
        LocalDate dateOfSunday = startingDateOfDisplay.with( fieldISO, DayOfWeek.SUNDAY.getValue( ) );
        LocalDate endingDateOfDisplay = dateOfSunday.plusWeeks( nNbWeeksToDisplay - 1L );
        LocalDate endingValidityDate = null;
        if ( appointmentForm.getDateEndValidity( ) != null )
        {
            endingValidityDate = appointmentForm.getDateEndValidity( ).toLocalDate( );
        }
        if ( endingValidityDate != null && endingDateOfDisplay.isAfter( endingValidityDate ) )
        {
            endingDateOfDisplay = endingValidityDate;
        }
        List<Slot> listSlots = SlotService.buildListSlot( appointmentForm.getIdForm( ),
                WeekDefinitionService.findAllWeekDefinition( appointmentForm.getIdForm( ) ), startingDateOfDisplay, endingDateOfDisplay );
        // Get the min time from now before a user can take an appointment (in hours)
        // Filter the list of slots
        if ( CollectionUtils.isNotEmpty( listSlots ) && appointmentForm.getMinTimeBeforeAppointment( ) != 0 )
        {
            LocalDateTime dateTimeBeforeAppointment = LocalDateTime.now( ).plusHours( appointmentForm.getMinTimeBeforeAppointment( ) );
            listSlots = listSlots.stream( ).filter( s -> s.getStartingDateTime( ).isAfter( dateTimeBeforeAppointment ) ).collect( Collectors.toList( ) );
        }

        return listSlots;
    }

    public static int calculateConsecutiveSlots( Slot slot, List<Slot> allSlots )
    {
        if ( slot.getNbPotentialRemainingPlaces( ) <= 0 )
        {
            return 0;
        }
        AtomicInteger consecutiveSlots = new AtomicInteger( 1 );
        doCalculateConsecutiveSlots( slot, allSlots, consecutiveSlots );
        return consecutiveSlots.get( );
    }

    private static void doCalculateConsecutiveSlots( Slot slot, List<Slot> allSlots, AtomicInteger consecutiveSlots )
    {
        for ( Slot nextSlot : allSlots )
        {
            if ( Objects.equals( slot.getEndingDateTime( ), nextSlot.getStartingDateTime( ) ) )
            {
                if ( nextSlot.getNbPotentialRemainingPlaces( ) > 0 && nextSlot.getIsOpen( ) )
                {
                    consecutiveSlots.addAndGet( 1 );
                    doCalculateConsecutiveSlots( nextSlot, allSlots, consecutiveSlots );
                }
                else
                {
                    break;
                }
            }
        }
    }
}