View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.appointment.modules.solr.service;
35  
36  import fr.paris.lutece.plugins.appointment.business.slot.Slot;
37  import fr.paris.lutece.plugins.appointment.service.SlotService;
38  import fr.paris.lutece.plugins.appointment.service.WeekDefinitionService;
39  import fr.paris.lutece.plugins.appointment.web.dto.AppointmentFormDTO;
40  import fr.paris.lutece.plugins.search.solr.indexer.SolrIndexerService;
41  import fr.paris.lutece.plugins.search.solr.indexer.SolrItem;
42  import fr.paris.lutece.portal.web.l10n.LocaleService;
43  import fr.paris.lutece.util.url.UrlItem;
44  import org.apache.commons.collections.CollectionUtils;
45  import org.apache.commons.lang3.StringUtils;
46  
47  import java.time.DayOfWeek;
48  import java.time.LocalDate;
49  import java.time.LocalDateTime;
50  import java.time.temporal.ChronoUnit;
51  import java.time.temporal.TemporalField;
52  import java.time.temporal.WeekFields;
53  import java.util.List;
54  import java.util.Objects;
55  import java.util.concurrent.atomic.AtomicInteger;
56  import java.util.stream.Collectors;
57  
58  /**
59   * Utils for the slots (Uid, Url, Item ...)
60   * 
61   * @author Laurent Payen
62   *
63   */
64  public final class SlotUtil
65  {
66  
67      private static final String DAY_OPEN = "day_open";
68      private static final String ENABLED = "enabled";
69      private static final String SLOT_NB_FREE_PLACES = "slot_nb_free_places";
70      private static final String SLOT_NB_PLACES = "slot_nb_places";
71      private static final String DAY_OF_WEEK = "day_of_week";
72      private static final String MINUTE_OF_DAY = "minute_of_day";
73      private static final String NB_CONSECUTIVES_SLOTS = "nb_consecutives_slots";
74      private static final String APPOINTMENT_MULTISLOTS = "appointment_multislots";
75      private static final String MAX_CONSECUTIVES_SLOTS = "max_consecutives_slots";
76      private static final String UID_FORM = "uid_form";
77      private static final String URL_FORM = "url_form";
78      private static final String APPOINTMENT_SLOT = "appointmentslot";
79      private static final String VIEW_FORM = "getViewAppointmentForm";
80  
81      private static final String PARAMETER_STARTING_DATETIME = "starting_date_time";
82      private static final String PARAMETER_ANCHOR = "anchor";
83      private static final String VALUE_ANCHOR = "step3";
84  
85      /**
86       * Private constructor - this class does not need to be instantiated
87       */
88      private SlotUtil( )
89      {
90      }
91  
92      /**
93       * Generate a unique ID for solr.
94       *
95       * 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,
96       * using only alphanumerical caracters to avoid potential problems with code parsing this ID.
97       * 
98       */
99      public static String getSlotUid( Slot slot )
100     {
101         String strSlotDateFormatted = slot.getStartingDateTime( ).format( Utilities.SLOT_SOLR_ID_DATE_FORMATTER );
102         return "F" + slot.getIdForm( ) + "D" + strSlotDateFormatted;
103     }
104 
105     /**
106      * Get the slot url to call directly rdv v2 with the good parameters
107      * 
108      * @param slot
109      *            the slot
110      * @return the url with all the parameters
111      */
112     public static String getSlotUrl( Slot slot )
113     {
114         UrlItem url = new UrlItem( SolrIndexerService.getBaseUrl( ) );
115         url.addParameter( Utilities.PARAMETER_XPAGE, Utilities.XPAGE_APPOINTMENT );
116         url.addParameter( Utilities.PARAMETER_VIEW, VIEW_FORM );
117         url.addParameter( FormUtil.PARAMETER_ID_FORM, slot.getIdForm( ) );
118         url.addParameter( PARAMETER_STARTING_DATETIME, slot.getStartingDateTime( ).toString( ) );
119         url.addParameter( PARAMETER_ANCHOR, VALUE_ANCHOR );
120         return url.getUrl( );
121     }
122 
123     /**
124      * Build and return the slot Item for Solr
125      * 
126      * @param appointmentForm
127      *            the Appointment Form
128      * @param slot
129      *            the slot
130      * @return the slot Item
131      */
132     public static SolrItem getSlotItem( AppointmentFormDTO appointmentForm, Slot slot, List<Slot> allSlots )
133     {
134         // the item
135         SolrItem item = FormUtil.getDefaultFormItem( appointmentForm );
136         item.setUid( Utilities.buildResourceUid( getSlotUid( slot ), Utilities.RESOURCE_TYPE_SLOT ) );
137         item.addDynamicFieldNotAnalysed( UID_FORM, FormUtil.getFormUid( appointmentForm.getIdForm( ) ) );
138         item.setUrl( getSlotUrl( slot ) );
139         item.addDynamicFieldNotAnalysed( URL_FORM, FormUtil.getFormUrl( appointmentForm.getIdForm( ) ) );
140         item.setDate( slot.getStartingTimestampDate( ) );
141         item.setType( Utilities.SHORT_NAME_SLOT );
142         if ( StringUtils.isNotEmpty( appointmentForm.getAddress( ) ) && appointmentForm.getLongitude( ) != null && appointmentForm.getLatitude( ) != null )
143         {
144             item.addDynamicFieldGeoloc( APPOINTMENT_SLOT, appointmentForm.getAddress( ), appointmentForm.getLongitude( ), appointmentForm.getLatitude( ),
145                     "appointmentslot-" + slot.getNbPotentialRemainingPlaces( ) + "/" + slot.getMaxCapacity( ) );
146         }
147         item.addDynamicFieldNotAnalysed( DAY_OPEN, String.valueOf( Boolean.TRUE ) );
148         item.addDynamicFieldNotAnalysed( ENABLED, String.valueOf( slot.getIsOpen( ) ) );
149         item.addDynamicField( SLOT_NB_FREE_PLACES, Long.valueOf( slot.getNbPotentialRemainingPlaces( ) ) );
150         item.addDynamicField( SLOT_NB_PLACES, Long.valueOf( slot.getMaxCapacity( ) ) );
151         item.addDynamicField( DAY_OF_WEEK, Long.valueOf( slot.getStartingDateTime( ).getDayOfWeek( ).getValue( ) ) );
152         item.addDynamicField( MINUTE_OF_DAY,
153                 ChronoUnit.MINUTES.between( slot.getStartingDateTime( ).toLocalDate( ).atStartOfDay( ), slot.getStartingDateTime( ) ) );
154 
155         long consecutiveSlots = calculateConsecutiveSlots(slot, allSlots);
156         item.addDynamicField( APPOINTMENT_MULTISLOTS, Boolean.toString( appointmentForm.getIsMultislotAppointment( ) ) );
157         if (appointmentForm.getIsMultislotAppointment()) {
158             item.addDynamicField(MAX_CONSECUTIVES_SLOTS, Long.valueOf(appointmentForm.getNbConsecutiveSlots()));
159             if (consecutiveSlots <= appointmentForm.getNbConsecutiveSlots()) {
160                 item.addDynamicField(NB_CONSECUTIVES_SLOTS, consecutiveSlots);
161             }
162             else
163             {
164                 item.addDynamicField(NB_CONSECUTIVES_SLOTS, (long) appointmentForm.getNbConsecutiveSlots());
165             }
166         } else {
167             item.addDynamicField(NB_CONSECUTIVES_SLOTS, 1L);
168             item.addDynamicField(MAX_CONSECUTIVES_SLOTS, 1L);
169         }
170 
171         // Date Hierarchy
172         item.setHieDate( slot.getStartingDateTime( ).toLocalDate( ).format( Utilities.HIE_DATE_FORMATTER ) );
173         return item;
174     }
175 
176     /**
177      * Get all the slots of a form by calling the method buildListSlot of the plugin RDV
178      * 
179      * @param appointmentForm
180      *            the appointment form
181      * @return all the slots of a form
182      */
183     public static List<Slot> getAllSlots( AppointmentFormDTO appointmentForm )
184     {
185         // Get the nb weeks to display
186         int nNbWeeksToDisplay = appointmentForm.getNbWeeksToDisplay( );
187         LocalDate startingDateOfDisplay = LocalDate.now( );
188         if ( appointmentForm.getDateStartValidity( ) != null && startingDateOfDisplay.isBefore( appointmentForm.getDateStartValidity( ).toLocalDate( ) ) )
189         {
190             startingDateOfDisplay = appointmentForm.getDateStartValidity( ).toLocalDate( );
191         }
192         // Calculate the ending date of display with the nb weeks to display
193         // since today
194         // We calculate the number of weeks including the current week, so it
195         // will end to the (n) next sunday
196         TemporalField fieldISO = WeekFields.of( LocaleService.getDefault( ) ).dayOfWeek( );
197         LocalDate dateOfSunday = startingDateOfDisplay.with( fieldISO, DayOfWeek.SUNDAY.getValue( ) );
198         LocalDate endingDateOfDisplay = dateOfSunday.plusWeeks( nNbWeeksToDisplay - 1L );
199         LocalDate endingValidityDate = null;
200         if ( appointmentForm.getDateEndValidity( ) != null )
201         {
202             endingValidityDate = appointmentForm.getDateEndValidity( ).toLocalDate( );
203         }
204         if ( endingValidityDate != null && endingDateOfDisplay.isAfter( endingValidityDate ) )
205         {
206             endingDateOfDisplay = endingValidityDate;
207         }
208         List<Slot> listSlots = SlotService.buildListSlot( appointmentForm.getIdForm( ),
209                 WeekDefinitionService.findAllWeekDefinition( appointmentForm.getIdForm( ) ), startingDateOfDisplay, endingDateOfDisplay );
210         // Get the min time from now before a user can take an appointment (in hours)
211         // Filter the list of slots
212         if ( CollectionUtils.isNotEmpty( listSlots ) && appointmentForm.getMinTimeBeforeAppointment( ) != 0 )
213         {
214             LocalDateTime dateTimeBeforeAppointment = LocalDateTime.now( ).plusHours( appointmentForm.getMinTimeBeforeAppointment( ) );
215             listSlots = listSlots.stream( ).filter( s -> s.getStartingDateTime( ).isAfter( dateTimeBeforeAppointment ) ).collect( Collectors.toList( ) );
216         }
217 
218         return listSlots;
219     }
220 
221     public static int calculateConsecutiveSlots( Slot slot, List<Slot> allSlots )
222     {
223         if ( slot.getNbPotentialRemainingPlaces( ) <= 0 )
224         {
225             return 0;
226         }
227         AtomicInteger consecutiveSlots = new AtomicInteger( 1 );
228         doCalculateConsecutiveSlots( slot, allSlots, consecutiveSlots );
229         return consecutiveSlots.get( );
230     }
231 
232     private static void doCalculateConsecutiveSlots( Slot slot, List<Slot> allSlots, AtomicInteger consecutiveSlots )
233     {
234         for ( Slot nextSlot : allSlots )
235         {
236             if ( Objects.equals( slot.getEndingDateTime( ), nextSlot.getStartingDateTime( ) ) )
237             {
238                 if ( nextSlot.getNbPotentialRemainingPlaces( ) > 0 && nextSlot.getIsOpen( ) )
239                 {
240                     consecutiveSlots.addAndGet( 1 );
241                     doCalculateConsecutiveSlots( nextSlot, allSlots, consecutiveSlots );
242                 }
243                 else
244                 {
245                     break;
246                 }
247             }
248         }
249     }
250 }