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.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      * Returns the content of the page AppointmentSearchApp.
149      * 
150      * @param request
151      *            The HTTP request
152      * @return The view
153      * @throws AccessDeniedException
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 ); // Count will be set
261                                                   // later with the
262                                                   // correct value
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                 // Here, we could add a difference between null and ""
306                 // by using
307                 // another special value (eg VALUE_FQ_MISSING =
308                 // __MISSING__).
309                 if ( StringUtils.isEmpty( strCode ) )
310                 {
311                     strCode = SolrQueryService.VALUE_FQ_EMPTY;
312                     strCodeName = SolrQueryService.VALUE_FQ_EMPTY;
313                     strName = strLabelEmpty;
314                 }
315             }
316             // Even though we set FacetMinCount to 1, solr gives a
317             // result with a count of 0 for the docs missing the field
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     // Getting the types right for all the maps and lists is just to tedious
378     // Working directly on the solr types would make it easier, but I don't
379     // want to mutate their objects directly in case it breaks their code.
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     // Getting the types right for all the maps and lists is just to tedious
401     // Working directly on the solr types would make it easier, but I don't
402     // want to mutate their objects directly in case it breaks their code.
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      * Process the search of the page solrsearchapp.
414      * 
415      * @param request
416      *            The HTTP request
417      * @return The view
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         // Need to put the category in the url
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      * Process the search of the page solrsearchapp.
444      * 
445      * @param request
446      *            The HTTP request
447      * @return The view
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         // Need to put the category in the url
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     // SolrDocumentList extends ArrayList and has extra methods
486     // But in Freemarker, we can't have access to those extra methods (like
487     // getNumFound())
488     // So unwrap everything.
489     // We could use ?api in freemarker version 2.3.22 but we are stuck with
490     // 2.3.21.
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 }