1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package fr.paris.lutece.plugins.appointment.modules.solrsearchapp.web;
35
36 import java.io.IOException;
37 import java.util.AbstractMap.SimpleImmutableEntry;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Comparator;
41 import java.util.Date;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.LinkedHashMap;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.Set;
49
50 import javax.servlet.http.HttpServletRequest;
51
52 import org.apache.commons.lang.StringUtils;
53 import org.apache.solr.client.solrj.SolrClient;
54 import org.apache.solr.client.solrj.SolrQuery;
55 import org.apache.solr.client.solrj.SolrServerException;
56 import org.apache.solr.client.solrj.response.FacetField;
57 import org.apache.solr.client.solrj.response.Group;
58 import org.apache.solr.client.solrj.response.GroupCommand;
59 import org.apache.solr.client.solrj.response.GroupResponse;
60 import org.apache.solr.client.solrj.response.PivotField;
61 import org.apache.solr.client.solrj.response.QueryResponse;
62 import org.apache.solr.common.SolrDocumentList;
63 import org.apache.solr.common.params.GroupParams;
64
65 import fr.paris.lutece.plugins.appointment.modules.solrsearchapp.service.SolrQueryService;
66 import fr.paris.lutece.plugins.appointment.modules.solrsearchapp.service.Utilities;
67 import fr.paris.lutece.plugins.search.solr.business.SolrServerService;
68 import fr.paris.lutece.portal.service.admin.AccessDeniedException;
69 import fr.paris.lutece.portal.service.i18n.I18nService;
70 import fr.paris.lutece.portal.service.util.AppLogService;
71 import fr.paris.lutece.portal.service.util.AppPropertiesService;
72 import fr.paris.lutece.portal.util.mvc.commons.annotations.Action;
73 import fr.paris.lutece.portal.util.mvc.commons.annotations.View;
74 import fr.paris.lutece.portal.util.mvc.xpage.MVCApplication;
75 import fr.paris.lutece.portal.util.mvc.xpage.annotations.Controller;
76 import fr.paris.lutece.portal.web.xpages.XPage;
77 import fr.paris.lutece.util.ReferenceItem;
78 import fr.paris.lutece.util.ReferenceList;
79
80 @Controller( xpageName = "appointmentsearch", pageTitleI18nKey = "module.appointment.solrsearchapp.pageTitle", pagePathI18nKey = "module.appointment.solrsearchapp.pagePathLabel" )
81 public class AppointmentSearchApp extends MVCApplication
82 {
83
84 private static final long serialVersionUID = 3579388931034541505L;
85
86 private static final int HUGE_INFINITY = 10000000;
87
88 private static final String PROPERTY_CATEGORY_REQUIRED = "appointment-solrsearchapp.app.category.required";
89 private static final boolean CATEGORY_REQUIRED_DEFAULT = false;
90
91 private static final String ACCESS_DENIED = "module.appointment.solrsearchapp.accessDenied";
92
93 private static final String VALUES = "values";
94 private static final String VIEW_SEARCH = "search";
95 private static final String ACTION_SEARCH = "search";
96 private static final String ACTION_CLEAR = "clear";
97 private static final String TEMPLATE_SEARCH = "skin/plugins/appointment/modules/solrsearchapp/search.html";
98 private static final String SOLR_FILTERQUERY_NOT_FULL = "NOT slot_nb_free_places_long:0";
99 private static final String SOLR_FIELD_NB_FREE_PLACES = "slot_nb_free_places_long";
100 private static final String SOLR_FIELD_NB_PLACES = "slot_nb_places_long";
101 private static final String SOLR_FIELD_FORM_UID = "uid_form_string";
102 private static final String SOLR_PIVOT_NB_FREE_PLACES = SOLR_FIELD_FORM_UID + "," + SOLR_FIELD_NB_FREE_PLACES;
103 private static final String SOLR_PIVOT_NB_PLACES = SOLR_FIELD_FORM_UID + "," + SOLR_FIELD_NB_PLACES;
104 private static final String MARK_ITEM_DAYS_OF_WEEK = "items_days_of_week";
105 private static final String MARK_SITE = "site";
106 private static final String MARK_CATEGORIE = "category";
107 private static final String MARK_FORM = "form";
108 private static final String MARK_FROM_DATE = "from_date";
109 private static final String MARK_FROM_TIME = "from_time";
110 private static final String MARK_TO_DATE = "to_date";
111 private static final String MARK_TO_TIME = "to_time";
112 private static final String MARK_FROM_DAY_MINUTE = "from_day_minute";
113 private static final String MARK_TO_DAY_MINUTE = "to_day_minute";
114 private static final String MARK_NB_SLOTS = "nb_consecutive_slots";
115 private static final String MARK_ROLE = "role";
116 private static final String MARK_RESULTS = "results";
117
118 private static final List<SimpleImmutableEntry<String, String>> SEARCH_FIELDS = Arrays.asList(
119 new SimpleImmutableEntry<>( Utilities.PARAMETER_SITE, MARK_SITE ), new SimpleImmutableEntry<>( Utilities.PARAMETER_CATEGORY, MARK_CATEGORIE ),
120 new SimpleImmutableEntry<>( Utilities.PARAMETER_FORM, MARK_FORM ), new SimpleImmutableEntry<>( Utilities.PARAMETER_FROM_DATE, MARK_FROM_DATE ),
121 new SimpleImmutableEntry<>( Utilities.PARAMETER_FROM_TIME, MARK_FROM_TIME ),
122 new SimpleImmutableEntry<>( Utilities.PARAMETER_TO_DATE, MARK_TO_DATE ), new SimpleImmutableEntry<>( Utilities.PARAMETER_TO_TIME, MARK_TO_TIME ),
123 new SimpleImmutableEntry<>( Utilities.PARAMETER_FROM_DAY_MINUTE, MARK_FROM_DAY_MINUTE ),
124 new SimpleImmutableEntry<>( Utilities.PARAMETER_TO_DAY_MINUTE, MARK_TO_DAY_MINUTE ),
125 new SimpleImmutableEntry<>( Utilities.PARAMETER_NB_SLOTS, MARK_NB_SLOTS ), new SimpleImmutableEntry<>( Utilities.PARAMETER_ROLE, MARK_ROLE ) );
126
127 private static final int SOLR_GROUP_LIMIT = 3;
128 private Map<String, String> _searchParameters;
129 private Map<String, String [ ]> _searchMultiParameters;
130
131 private void initSearchParameters( )
132 {
133 if ( _searchParameters == null )
134 {
135 _searchParameters = new HashMap<>( );
136 _searchMultiParameters = new HashMap<>( );
137 _searchMultiParameters.put( Utilities.PARAMETER_DAYS_OF_WEEK, Utilities.LIST_DAYS_CODE );
138 _searchParameters.put( Utilities.PARAMETER_FROM_DAY_MINUTE, "360" );
139 _searchParameters.put( Utilities.PARAMETER_TO_DAY_MINUTE, "1260" );
140 _searchParameters.put( Utilities.PARAMETER_FROM_TIME, "06:00" );
141 _searchParameters.put( Utilities.PARAMETER_TO_TIME, "21:00" );
142 _searchParameters.put( Utilities.PARAMETER_NB_SLOTS, "1" );
143 _searchParameters.put( Utilities.PARAMETER_ROLE, "none" );
144 }
145 }
146
147
148
149
150
151
152
153
154
155 @View( value = VIEW_SEARCH, defaultView = true )
156 public XPage viewSearch( HttpServletRequest request )
157 {
158 Map<String, Object> model = new HashMap<>( );
159 String category = request.getParameter( Utilities.PARAMETER_CATEGORY );
160 if ( AppPropertiesService.getPropertyBoolean( PROPERTY_CATEGORY_REQUIRED, CATEGORY_REQUIRED_DEFAULT ) && StringUtils.isEmpty( category ) )
161 {
162 addError( ACCESS_DENIED, getLocale( request ) );
163 model = getModel( );
164 return getXPage( TEMPLATE_SEARCH, request.getLocale( ), model );
165 }
166 initSearchParameters( );
167 Locale locale = request.getLocale( );
168
169 for ( SimpleImmutableEntry<String, String> entry : SEARCH_FIELDS )
170 {
171 String strValue = Utilities.getSearchParameter( entry.getKey( ), request, _searchParameters );
172 if ( StringUtils.isNotBlank( strValue ) )
173 {
174 model.put( entry.getValue( ), strValue );
175 }
176 }
177
178 SolrClient solrServer = SolrServerService.getInstance( ).getSolrServer( );
179 if ( solrServer == null )
180 {
181 AppLogService.error( "AppointmentSolr error, getSolrServer returns null" );
182 }
183
184 SolrQuery basedQuery = SolrQueryService.getCommonFilteredQuery( request, _searchParameters, _searchMultiParameters );
185 SolrQuery queryAllPlaces = basedQuery;
186 queryAllPlaces.setRows( 0 );
187 queryAllPlaces.addFacetPivotField( SOLR_PIVOT_NB_PLACES );
188 QueryResponse responseAllPlaces = null;
189 try
190 {
191 responseAllPlaces = solrServer.query( queryAllPlaces );
192 }
193 catch( SolrServerException | IOException e )
194 {
195 AppLogService.error( "AppointmentSolr error, exception during queryAllPlaces", e );
196 }
197 if ( responseAllPlaces != null )
198 {
199 HashMap<String, Integer> mapPlacesCount = getPlacesCount( responseAllPlaces, SOLR_PIVOT_NB_PLACES );
200 model.put( "totalPlacesCount", mapPlacesCount );
201 }
202
203 SolrQuery query = basedQuery;
204 query.setRows( HUGE_INFINITY );
205 query.addFilterQuery( SOLR_FILTERQUERY_NOT_FULL );
206 query.addSort( SolrQueryService.SOLR_FIELD_DATE, SolrQuery.ORDER.asc );
207 query.set( GroupParams.GROUP, true );
208 query.set( GroupParams.GROUP_FIELD, SOLR_FIELD_FORM_UID );
209 query.set( GroupParams.GROUP_LIMIT, SOLR_GROUP_LIMIT );
210 query.setFacet( true );
211 query.addFacetPivotField( SOLR_PIVOT_NB_FREE_PLACES );
212 query.setFacetMinCount( 1 );
213 query.setFacetMissing( true );
214
215 QueryResponse response = null;
216 try
217 {
218 response = solrServer.query( query );
219 }
220 catch( SolrServerException | IOException e )
221 {
222 AppLogService.error( "AppointmentSolr error, exception during query", e );
223 }
224 if ( response == null )
225 {
226 return getXPage( TEMPLATE_SEARCH, request.getLocale( ), model );
227 }
228 GroupResponse groupResponse = response.getGroupResponse( );
229 HashMap<String, Integer> mapFreePlacesCount = getPlacesCount( response, SOLR_PIVOT_NB_FREE_PLACES );
230
231 Map<String, Object> wrapGroupResponse = wrapGroupResponse( groupResponse );
232 sortResponses( wrapGroupResponse, mapFreePlacesCount );
233
234 model.put( MARK_RESULTS, wrapGroupResponse );
235
236 model.put( "freePlacesCount", mapFreePlacesCount );
237
238 for ( SimpleImmutableEntry<String, String> entry : SolrQueryService.FACET_FIELDS )
239 {
240 ReferenceList referenceList = createReferenceListFacet( response, entry, request, locale );
241 model.put( entry.getValue( ), referenceList );
242 }
243
244 FacetField facetField = response.getFacetField( SolrQueryService.SOLR_FIELD_DAY_OF_WEEK );
245 ReferenceList referenceListDaysOfWeek = createReferenceListDaysOfWeek( facetField, request, locale );
246 model.put( MARK_ITEM_DAYS_OF_WEEK, referenceListDaysOfWeek );
247
248 String nbSlots = Utilities.getSearchParameterValue( Utilities.PARAMETER_NB_SLOTS, request, _searchParameters );
249 model.put( MARK_NB_SLOTS, nbSlots );
250 return getXPage( TEMPLATE_SEARCH, request.getLocale( ), model );
251 }
252
253 private ReferenceList createReferenceListFacet( QueryResponse response, SimpleImmutableEntry<String, String> entry, HttpServletRequest request,
254 Locale locale )
255 {
256 String strLabelAll = I18nService.getLocalizedString( "module.appointment.solrsearchapp.labelFilterAll", locale );
257 String strLabelEmpty = I18nService.getLocalizedString( "module.appointment.solrsearchapp.labelFilterEmpty", locale );
258
259 ReferenceList referenceList = new ReferenceList( );
260 referenceList.addItem( "", strLabelAll );
261
262
263
264 FacetField facetField = response.getFacetField( entry.getKey( ) );
265 String strSearchParameter;
266 boolean bFacetAndLabel;
267 if ( SolrQueryService.SOLR_FIELD_FORM_UID_TITLE.equals( entry.getKey( ) ) )
268 {
269 bFacetAndLabel = true;
270 strSearchParameter = Utilities.PARAMETER_FORM;
271 }
272 else
273 {
274 bFacetAndLabel = false;
275 strSearchParameter = SolrQueryService.EXACT_FACET_QUERIES.stream( ).filter( fq -> fq.getKey( ).equals( entry.getKey( ) ) ).findFirst( )
276 .map( SimpleImmutableEntry::getValue ).orElse( Utilities.PARAMETER_CATEGORY );
277 }
278 boolean bCurrentSearchParameterPresent = false;
279 String strSearchParameterValue = Utilities.getSearchParameterValue( strSearchParameter, request, _searchParameters );
280
281 int total = 0;
282 for ( FacetField.Count facetFieldCount : facetField.getValues( ) )
283 {
284 String strCode;
285 String strName;
286 String strCodeName;
287
288 if ( facetFieldCount.getName( ) == null )
289 {
290 bFacetAndLabel = false;
291 }
292
293 if ( bFacetAndLabel )
294 {
295 String [ ] facetNameSplit = facetFieldCount.getName( ).split( "\\|" );
296 strCode = facetNameSplit [0];
297 strName = facetNameSplit [1];
298 strCodeName = facetNameSplit [0] + "|" + facetNameSplit [1];
299 }
300 else
301 {
302 strCode = facetFieldCount.getName( );
303 strName = facetFieldCount.getName( );
304 strCodeName = facetFieldCount.getName( );
305
306
307
308
309 if ( StringUtils.isEmpty( strCode ) )
310 {
311 strCode = SolrQueryService.VALUE_FQ_EMPTY;
312 strCodeName = SolrQueryService.VALUE_FQ_EMPTY;
313 strName = strLabelEmpty;
314 }
315 }
316
317
318 if ( facetFieldCount.getCount( ) > 0 )
319 {
320 referenceList.addItem( strCodeName, strName + " (" + facetFieldCount.getCount( ) + ")" );
321 bCurrentSearchParameterPresent |= strCode.equals( strSearchParameterValue );
322 total += facetFieldCount.getCount( );
323 }
324 }
325
326 ReferenceItem itemAll = referenceList.get( 0 );
327 itemAll.setName( itemAll.getName( ) + " (" + total + ")" );
328
329 if ( !bCurrentSearchParameterPresent && StringUtils.isNotEmpty( strSearchParameterValue ) )
330 {
331 String strSearchParameterValueLabel = Utilities.getSearchParameter( strSearchParameter, request, _searchParameters );
332 String strSearchParameterLabel = Utilities.getSearchParameterLabel( strSearchParameter, request, _searchParameters );
333 referenceList.addItem( strSearchParameterValueLabel, strSearchParameterLabel + " (0)" );
334 }
335
336 return referenceList;
337 }
338
339 private ReferenceList createReferenceListDaysOfWeek( FacetField facetField, HttpServletRequest request, Locale locale )
340 {
341 Set<String> searchDaysChecked = new HashSet<>( );
342 ReferenceList referenceListDaysOfWeek = new ReferenceList( );
343 String [ ] searchDays = Utilities.getSearchMultiParameter( Utilities.PARAMETER_DAYS_OF_WEEK, request, _searchMultiParameters );
344 if ( searchDays != null )
345 {
346 searchDaysChecked.addAll( Arrays.asList( searchDays ) );
347 }
348 if ( searchDaysChecked.isEmpty( ) )
349 {
350 searchDaysChecked.addAll( Arrays.asList( Utilities.LIST_DAYS_CODE ) );
351 }
352 Map<String, FacetField.Count> searchDaysCounts = new HashMap<>( );
353 for ( FacetField.Count facetFieldCount : facetField.getValues( ) )
354 {
355 searchDaysCounts.put( facetFieldCount.getName( ), facetFieldCount );
356 }
357
358 for ( SimpleImmutableEntry<String, String> day : Utilities.LIST_DAYS )
359 {
360 String strDayCode = day.getKey( );
361 String strDayLabel = I18nService.getLocalizedString( day.getValue( ), locale );
362 ReferenceItem item = new ReferenceItem( );
363 item.setCode( strDayCode );
364 long count = 0;
365 FacetField.Count facetFieldCount = searchDaysCounts.get( strDayCode );
366 if ( facetFieldCount != null )
367 {
368 count = facetFieldCount.getCount( );
369 }
370 item.setName( strDayLabel + " (" + count + ")" );
371 item.setChecked( searchDaysChecked.contains( strDayCode ) );
372 referenceListDaysOfWeek.add( item );
373 }
374 return referenceListDaysOfWeek;
375 }
376
377
378
379
380 @SuppressWarnings( {
381 "rawtypes", "unchecked"
382 } )
383 private void sortResponses( Map<String, Object> wrapGroupResponse, HashMap<String, Integer> mapFreePlacesCount )
384 {
385 List<Object> listGroupCommands = (List) wrapGroupResponse.get( VALUES );
386 List<Map> listGroups = ( (List) ( (Map) listGroupCommands.get( 0 ) ).get( VALUES ) );
387
388 listGroups.sort( Comparator.comparing( ( Map group ) -> {
389 Map firstResult = getFirstResult( group );
390 return (Date) firstResult.get( "date" );
391 } ).thenComparing( ( Map group ) -> {
392 String code = (String) group.get( "groupValue" );
393 return mapFreePlacesCount.get( code );
394 }, Comparator.reverseOrder( ) ).thenComparing( ( Map group ) -> {
395 Map firstResult = getFirstResult( group );
396 return (String) firstResult.get( "title" );
397 } ) );
398 }
399
400
401
402
403 @SuppressWarnings( "rawtypes" )
404 private Map getFirstResult( Object group )
405 {
406 Map mapGroup = (Map) group;
407 Map mapResult = (Map) mapGroup.get( "result" );
408 List listResult = (List) mapResult.get( "list" );
409 return (Map) listResult.get( 0 );
410 }
411
412
413
414
415
416
417
418
419 @Action( value = ACTION_SEARCH )
420 public XPage doSearch( HttpServletRequest request )
421 {
422 initSearchParameters( );
423 for ( SimpleImmutableEntry<String, String> entry : SEARCH_FIELDS )
424 {
425 String strValue = request.getParameter( entry.getKey( ) );
426 if ( strValue != null )
427 {
428 _searchParameters.put( entry.getKey( ), strValue );
429 }
430 }
431
432 _searchMultiParameters.put( Utilities.PARAMETER_DAYS_OF_WEEK, request.getParameterValues( Utilities.PARAMETER_DAYS_OF_WEEK ) );
433
434 LinkedHashMap<String, String> additionalParameters = new LinkedHashMap<>( );
435 if ( StringUtils.isNotEmpty( request.getParameter( Utilities.PARAMETER_CATEGORY ) ) )
436 {
437 additionalParameters.put( Utilities.PARAMETER_CATEGORY, request.getParameter( Utilities.PARAMETER_CATEGORY ) );
438 }
439 return redirect( request, VIEW_SEARCH, additionalParameters );
440 }
441
442
443
444
445
446
447
448
449 @Action( value = ACTION_CLEAR )
450 public XPage doClear( HttpServletRequest request )
451 {
452 initSearchParameters( );
453 _searchParameters.clear( );
454 _searchMultiParameters.clear( );
455 _searchMultiParameters.put( Utilities.PARAMETER_DAYS_OF_WEEK, Utilities.LIST_DAYS_CODE );
456 _searchParameters.put( Utilities.PARAMETER_FROM_DAY_MINUTE, "360" );
457 _searchParameters.put( Utilities.PARAMETER_TO_DAY_MINUTE, "1260" );
458 _searchParameters.put( Utilities.PARAMETER_FROM_TIME, "06:00" );
459 _searchParameters.put( Utilities.PARAMETER_TO_TIME, "21:00" );
460 _searchParameters.put( Utilities.PARAMETER_NB_SLOTS, "1" );
461 _searchParameters.put( Utilities.PARAMETER_ROLE, "none" );
462
463
464 LinkedHashMap<String, String> additionalParameters = new LinkedHashMap<>( );
465 additionalParameters.put( Utilities.PARAMETER_CATEGORY, request.getParameter( Utilities.PARAMETER_CATEGORY ) );
466 return redirect( request, VIEW_SEARCH, additionalParameters );
467 }
468
469 private HashMap<String, Integer> getPlacesCount( QueryResponse response, String strPivotName )
470 {
471 HashMap<String, Integer> mapPlacesCount = new HashMap<>( );
472 List<PivotField> listPivotField = response.getFacetPivot( ).get( strPivotName );
473 for ( PivotField pivotFieldUid : listPivotField )
474 {
475 int total = 0;
476 for ( PivotField pivotFieldPlaces : pivotFieldUid.getPivot( ) )
477 {
478 total += (Long) pivotFieldPlaces.getValue( ) * pivotFieldPlaces.getCount( );
479 }
480 mapPlacesCount.put( (String) pivotFieldUid.getValue( ), total );
481 }
482 return mapPlacesCount;
483 }
484
485
486
487
488
489
490
491 private Map<String, Object> wrapGroupResponse( GroupResponse groupResponse )
492 {
493 Map<String, Object> result = new HashMap<>( );
494 List<Map<String, Object>> wrappedListGroupCommand = new ArrayList<>( groupResponse.getValues( ).size( ) );
495 for ( GroupCommand groupCommand : groupResponse.getValues( ) )
496 {
497 wrappedListGroupCommand.add( wrapGroupCommand( groupCommand ) );
498 }
499 result.put( VALUES, wrappedListGroupCommand );
500 return result;
501 }
502
503 private Map<String, Object> wrapGroupCommand( GroupCommand groupCommand )
504 {
505 Map<String, Object> result = new HashMap<>( );
506 List<Map<String, Object>> wrappedListGroup = new ArrayList<>( groupCommand.getValues( ).size( ) );
507 for ( Group group : groupCommand.getValues( ) )
508 {
509 wrappedListGroup.add( wrapGroup( group ) );
510 }
511 result.put( VALUES, wrappedListGroup );
512 result.put( "matches", groupCommand.getMatches( ) );
513 result.put( "name", groupCommand.getName( ) );
514 result.put( "nGroups", groupCommand.getNGroups( ) );
515 return result;
516 }
517
518 private Map<String, Object> wrapGroup( Group group )
519 {
520 Map<String, Object> result = new HashMap<>( );
521 result.put( "result", wrapSolrDocumentList( group.getResult( ) ) );
522 result.put( "groupValue", group.getGroupValue( ) );
523 return result;
524 }
525
526 private Map<String, Object> wrapSolrDocumentList( SolrDocumentList solrDocumentList )
527 {
528 Map<String, Object> result = new HashMap<>( );
529 result.put( "maxScore", solrDocumentList.getMaxScore( ) );
530 result.put( "numFound", solrDocumentList.getNumFound( ) );
531 result.put( "start", solrDocumentList.getStart( ) );
532 result.put( "list", solrDocumentList );
533 return result;
534 }
535 }