001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.search.solr;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.collections.CollectionUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.solr.client.solrj.SolrClient;
035import org.apache.solr.client.solrj.SolrQuery;
036import org.apache.solr.client.solrj.SolrQuery.ORDER;
037import org.apache.solr.client.solrj.SolrRequest.METHOD;
038import org.apache.solr.client.solrj.response.FacetField;
039import org.apache.solr.client.solrj.response.FacetField.Count;
040import org.apache.solr.client.solrj.response.QueryResponse;
041import org.slf4j.Logger;
042
043import org.ametys.cms.search.SearchField;
044import org.ametys.cms.search.SearchResults;
045import org.ametys.cms.search.Sort;
046import org.ametys.cms.search.Sort.Order;
047import org.ametys.cms.search.query.Query;
048import org.ametys.cms.search.query.QuerySyntaxException;
049import org.ametys.core.group.GroupIdentity;
050import org.ametys.core.group.GroupManager;
051import org.ametys.core.right.AllowedUsers;
052import org.ametys.core.user.CurrentUserProvider;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.core.util.URIUtils;
055import org.ametys.plugins.repository.AmetysObject;
056import org.ametys.plugins.repository.AmetysObjectIterable;
057import org.ametys.plugins.repository.AmetysObjectResolver;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059
060/**
061 * Component searching objects corresponding to a {@link Query}.
062 */
063public class SearcherFactory extends AbstractLogEnabled implements Component, Serviceable, Initializable
064{
065    
066    /** The component role. */
067    public static final String ROLE = SearcherFactory.class.getName();
068    
069    /** The {@link AmetysObjectResolver} */
070    protected AmetysObjectResolver _resolver;
071    
072    /** The solr client provider */
073    protected SolrClientProvider _solrClientProvider;
074    
075    /** The current user provider. */
076    protected CurrentUserProvider _currentUserProvider;
077    
078    /** The group manager */
079    protected GroupManager _groupManager;
080    
081    /** The solr client */
082    protected SolrClient _solrClient;
083    
084    @Override
085    public void service(ServiceManager serviceManager) throws ServiceException
086    {
087        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
088        _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE);
089        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
090        _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE);
091    }
092    
093    @Override
094    public void initialize() throws Exception
095    {
096        _solrClient = _solrClientProvider.getReadClient();
097    }
098    
099    /**
100     * Create a Searcher.
101     * @return a Searcher object.
102     */
103    public Searcher create()
104    {
105        return new Searcher(getLogger());
106    }
107    
108    /**
109     * Class searching objects corresponding to a query, with optional sort, facets, and so on.
110     */
111    public class Searcher
112    {
113        private Logger _logger;
114        
115        private String _queryString;
116        private Query _query;
117        private List<String> _filterQueryStrings;
118        private List<Query> _filterQueries;
119        private List<Sort> _sortClauses;
120        private List<SearchField> _facets;
121        private Map<String, List<String>> _facetValues;
122        private int _start;
123        private int _maxResults;
124        private Map<String, Object> _searchContext;
125        private boolean _checkRights;
126        private AllowedUsers _checkRightsComparingTo;
127        private boolean _debug;
128        
129        /**
130         * Build a Searcher with default values.
131         * @param logger The logger.
132         */
133        protected Searcher(Logger logger)
134        {
135            _logger = logger;
136            
137            _filterQueryStrings = new ArrayList<>();
138            _filterQueries = new ArrayList<>();
139            _sortClauses = new ArrayList<>();
140            _facets = new ArrayList<>();
141            _facetValues = new HashMap<>();
142            _start = 0;
143            _maxResults = Integer.MAX_VALUE;
144            _searchContext = new HashMap<>();
145            _checkRights = true;
146        }
147        
148        /**
149         * Set the query (as a String).
150         * @param query the query (as a String).
151         * @return The Searcher object itself.
152         */
153        public Searcher withQueryString(String query)
154        {
155            if (this._query != null)
156            {
157                throw new IllegalArgumentException("Query and query string can't be used at the same time.");
158            }
159            this._queryString = query;
160            return this;
161        }
162        
163        /**
164         * Set the query (as a {@link Query} object).
165         * @param query the query (as a {@link Query} object).
166         * @return The Searcher object itself.
167         */
168        public Searcher withQuery(Query query)
169        {
170            if (this._queryString != null)
171            {
172                throw new IllegalArgumentException("Query and query string can't be used at the same time.");
173            }
174            this._query = query;
175            return this;
176        }
177        
178        /**
179         * Set the filter queries (as Strings).
180         * @param queries the filter queries (as Strings).
181         * @return The Searcher object itself. The Searcher object itself.
182         */
183        public Searcher withFilterQueryStrings(String... queries)
184        {
185            _filterQueryStrings = new ArrayList<>(queries.length);
186            CollectionUtils.addAll(_filterQueryStrings, queries);
187            return this;
188        }
189        
190        /**
191         * Set the filter queries (as Strings).
192         * @param queries the filter queries (as Strings).
193         * @return The Searcher object itself. The Searcher object itself.
194         */
195        public Searcher withFilterQueryStrings(Collection<String> queries)
196        {
197            _filterQueryStrings = new ArrayList<>(queries);
198            return this;
199        }
200        
201        /**
202         * Add a filter query to the existing ones (as a String).
203         * @param query the filter query to add (as a String).
204         * @return The Searcher object itself. The Searcher object itself.
205         */
206        public Searcher addFilterQueryString(String query)
207        {
208            _filterQueryStrings.add(query);
209            return this;
210        }
211        
212        /**
213         * Set the filter queries (as {@link Query} objects).
214         * @param queries the filter queries (as {@link Query} objects).
215         * @return The Searcher object itself. The Searcher object itself.
216         */
217        public Searcher withFilterQueries(Query... queries)
218        {
219            _filterQueries = new ArrayList<>(queries.length);
220            CollectionUtils.addAll(_filterQueries, queries);
221            return this;
222        }
223        
224        /**
225         * Set the filter queries (as {@link Query} objects).
226         * @param queries the filter queries (as {@link Query} objects).
227         * @return The Searcher object itself. The Searcher object itself.
228         */
229        public Searcher withFilterQueries(Collection<Query> queries)
230        {
231            _filterQueries = new ArrayList<>(queries);
232            return this;
233        }
234        
235        /**
236         * Add a filter query to the existing ones (as a {@link Query} object).
237         * @param query the filter query to add (as a {@link Query} object).
238         * @return The Searcher object itself. The Searcher object itself.
239         */
240        public Searcher addFilterQuery(Query query)
241        {
242            _filterQueries.add(query);
243            return this;
244        }
245        
246        /**
247         * Set the sort clauses.
248         * @param sortClauses the sort clauses.
249         * @return The Searcher object itself.
250         */
251        public Searcher withSort(Sort... sortClauses)
252        {
253            _sortClauses = new ArrayList<>(sortClauses.length);
254            CollectionUtils.addAll(_sortClauses, sortClauses);
255            return this;
256        }
257        
258        /**
259         * Set the sort clauses.
260         * @param sortClauses the sort clauses.
261         * @return The Searcher object itself.
262         */
263        public Searcher withSort(List<Sort> sortClauses)
264        {
265            _sortClauses = new ArrayList<>(sortClauses);
266            return this;
267        }
268        
269        /**
270         * Add a sort clause to the existing ones.
271         * @param sortClause The sort clause to add.
272         * @return The Searcher object itself.
273         */
274        public Searcher addSort(Sort sortClause)
275        {
276            _sortClauses.add(sortClause);
277            return this;
278        }
279        
280        /**
281         * Set the faceted fields.
282         * @param facets the faceted fields.
283         * @return The Searcher object itself.
284         */
285        public Searcher withFacets(SearchField... facets)
286        {
287            _facets = new ArrayList<>(facets.length);
288            CollectionUtils.addAll(_facets, facets);
289            return this;
290        }
291        
292        /**
293         * Set the faceted fields.
294         * @param facets the faceted fields.
295         * @return The Searcher object itself.
296         */
297        public Searcher withFacets(Collection<SearchField> facets)
298        {
299            _facets = new ArrayList<>(facets);
300            return this;
301        }
302        
303        /**
304         * Add a faceted field.
305         * @param facet The faceted field to add.
306         * @return The Searcher object itself.
307         */
308        public Searcher addFacet(SearchField facet)
309        {
310            _facets.add(facet);
311            return this;
312        }
313        
314        /**
315         * Set the facet values.
316         * @param facetValues The facet values.
317         * @return The Searcher object itself.
318         */
319        public Searcher withFacetValues(Map<String, List<String>> facetValues)
320        {
321            _facetValues = new HashMap<>(facetValues);
322            return this;
323        }
324        
325        /**
326         * Set the search offset and limit.
327         * @param start The start index (offset).
328         * @param maxResults The maximum number of results.
329         * @return The Searcher object itself.
330         */
331        public Searcher withLimits(int start, int maxResults)
332        {
333            this._start = start;
334            this._maxResults = maxResults;
335            return this;
336        }
337        
338        /**
339         * Set the search context.
340         * @param searchContext The search context.
341         * @return The Searcher object itself.
342         */
343        public Searcher withContext(Map<String, Object> searchContext)
344        {
345            _searchContext = new HashMap<>(searchContext);
346            return this;
347        }
348        
349        /**
350         * Add a value to the search context.
351         * @param key The context key.
352         * @param value The value.
353         * @return The Searcher object itself.
354         */
355        public Searcher addContextElement(String key, Object value)
356        {
357            _searchContext.put(key, value);
358            return this;
359        }
360        
361        /**
362         * Whether to check rights when searching, false otherwise.
363         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
364         * @return The Searcher object itself.
365         */
366        public Searcher setCheckRights(boolean checkRights)
367        {
368            _checkRights = checkRights;
369            return this;
370        }
371        
372        /**
373         * Check rights when searching, <b>not</b> according to the current user, 
374         * but according to the given {@link AllowedUsers visibilty} to compare each
375         * result with.
376         * @param compareTo the {@link AllowedUsers visibilty} to compare each result with.
377         * @return The Searcher object itself.
378         */
379        public Searcher checkRightsComparingTo(AllowedUsers compareTo)
380        {
381            _checkRights = false;
382            _checkRightsComparingTo = compareTo;
383            return this;
384        }
385        
386        /**
387         * Sets the debug on the Solr query 
388         * @return The Searcher object itself.
389         */
390        public Searcher setDebugOn()
391        {
392            _debug = true;
393            return this;
394        }
395        
396        /**
397         * Execute the search with the current parameters.
398         * @param <A> The type of search results
399         * @return An iterable on the result ametys objects.
400         * @throws Exception If an error occurs.
401         */
402        public <A extends AmetysObject> AmetysObjectIterable<A> search() throws Exception
403        {
404            SearchResults<A> searchResults = searchWithFacets();
405            return searchResults.getObjects();
406        }
407        
408        /**
409         * Execute the search with the current parameters.
410         * @param <A> The type of search results
411         * @return An iterable on the search result objects.
412         * @throws Exception If an error occurs.
413         */
414        public <A extends AmetysObject> SearchResults<A> searchWithFacets() throws Exception
415        {
416            QueryResponse response = _querySolrServer();
417            return _buildResults(response, _facets);
418        }
419        
420        /**
421         * From the Solr server response, builds the {@link SearchResults} object.
422         * @param <A> The type of search results
423         * @param response The response from the Solr server
424         * @param facets The facet fields to return
425         * @return An iterable on the search result objects.
426         * @throws Exception If an error occurs.
427         */
428        protected <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<SearchField> facets) throws Exception
429        {
430            _handleDebug(response);
431            Map<String, Map<String, Integer>> facetResults = getFacetResults(response, facets);
432            return new SolrSearchResults<>(response, _resolver, facetResults);
433        }
434        
435        private void _handleDebug(QueryResponse response)
436        {
437            if (_debug && _logger.isDebugEnabled())
438            {
439                Map<String, Object> debugMap = response.getDebugMap();
440                _logger.debug("Debug response: \n{}", debugMap);
441            }
442        }
443        
444        private QueryResponse _querySolrServer() throws Exception
445        {
446            _logSearchQueries();
447            String query = getQuery();
448            List<String> filterQueries = getFilterQueries();
449            
450            SolrQuery solrQuery = getSolrQuery(query, filterQueries, _start, _maxResults, _searchContext, _checkRights, _checkRightsComparingTo);
451            
452            // Set the sort specification and facets in the solr query object.
453            setSort(solrQuery, _sortClauses);
454            setFacets(solrQuery, _facets, _facetValues);
455            
456            modifySolrQuery(solrQuery);
457            
458            if (_logger.isInfoEnabled())
459            {
460                _logger.info("Solr query: " + URIUtils.decode(solrQuery.toString()));
461            }
462            
463            // Use POST to avoid problems with large requests.
464            return _solrClient.query(_solrClientProvider.getCollectionName(), solrQuery, METHOD.POST);
465        }
466        
467        private void _logSearchQueries()
468        {
469            if (!_logger.isDebugEnabled())
470            {
471                return;
472            }
473            
474            if (_queryString == null && _query != null)
475            {
476                _logger.debug("Query before building: \n{}", _query.toString(0));
477            }
478            if (!_filterQueries.isEmpty())
479            {
480                _logger.debug("Filter Queries before building: \n{}", _filterQueries
481                        .stream()
482                        .map(fq -> fq.toString(0))
483                        .collect(Collectors.joining("\n###\n")));
484            }
485        }
486        
487        /**
488         * Get the query string from the parameters.
489         * @return The query string.
490         * @throws QuerySyntaxException If the query is invalid.
491         */
492        protected String getQuery() throws QuerySyntaxException
493        {
494            String query = "*:*";
495            if (_queryString != null)
496            {
497                query = _queryString;
498            }
499            else if (_query != null)
500            {
501                query = _query.build();
502            }
503            return query;
504        }
505        
506        /**
507         * Get the filter queries from the parameters.
508         * @return The list of filter queries.
509         * @throws QuerySyntaxException If one of the queries is invalid.
510         */
511        protected List<String> getFilterQueries() throws QuerySyntaxException
512        {
513            List<String> filterQueries = new ArrayList<>();
514            if (!_filterQueryStrings.isEmpty())
515            {
516                filterQueries = _filterQueryStrings;
517            }
518            
519            if (!_filterQueries.isEmpty())
520            {
521                for (Query fq : _filterQueries)
522                {
523                    filterQueries.add(fq.build());
524                }
525            }
526            
527            return filterQueries;
528        }
529        
530        /**
531         * Get the solr query object.
532         * @param query The solr query string.
533         * @param filterQueries The filter queries (as Strings).
534         * @param start The start index.
535         * @param maxResults The maximum number of results.
536         * @param searchContext The search context.
537         * @param checkRights Whether to check rights when searching or not.
538         * @param allowedUsersToCompare The {@link AllowedUsers} object to compare with for checking rights
539         * @return The solr query object.
540         * @throws Exception If an error occurs.
541         */
542        protected SolrQuery getSolrQuery(String query, Collection<String> filterQueries, int start, int maxResults, Map<String, Object> searchContext, boolean checkRights, AllowedUsers allowedUsersToCompare) throws Exception
543        {
544            SolrQuery solrQuery = new SolrQuery();
545            
546            String queryString = StringUtils.isNotBlank(query) ? query : "*:*";
547            
548            // Set the query string, pagination spec and fields to be returned.
549            solrQuery.setQuery(queryString);
550            solrQuery.setStart(start);
551            solrQuery.setRows(maxResults);
552            // Return only ID + score fields.
553            solrQuery.setFields("id", "score");
554            
555            // Add filter queries.
556            for (String fq : filterQueries)
557            {
558                solrQuery.addFilterQuery(fq);
559            }
560            
561            if (checkRights)
562            {
563                _checkRightsQuery(solrQuery);
564            }
565            else if (allowedUsersToCompare != null)
566            {
567                _checkAllowedUsers(solrQuery, allowedUsersToCompare);
568            }
569            
570            if (_debug)
571            {
572                solrQuery.setShowDebugInfo(true);
573            }
574            
575            return solrQuery;
576        }
577        
578        private void _checkRightsQuery(SolrQuery solrQuery)
579        {
580            StringBuilder aclFilterQuery = new StringBuilder("{!acl ");
581            UserIdentity user = _currentUserProvider.getUser();
582            if (user == null)
583            {
584                aclFilterQuery.append("anonymous=");
585            }
586            else
587            {
588                aclFilterQuery.append("populationId=");
589                aclFilterQuery.append(user.getPopulationId());
590                aclFilterQuery.append(" login=");
591                aclFilterQuery.append(user.getLogin());
592                Set<GroupIdentity> groups = _groupManager.getUserGroups(user);
593                if (!groups.isEmpty())
594                {
595                    String groupsAsStr = groups.stream()
596                            .map(GroupIdentity::groupIdentityToString)
597                            .collect(Collectors.joining(","));
598                    aclFilterQuery.append(" groups=").append('"');
599                    aclFilterQuery.append(groupsAsStr).append('"');
600                }
601            }
602            aclFilterQuery.append("}");
603            // {!acl anonymous=} or {!acl populationId=users login=user1 groups="group1#groupDirectory,group2#groupDirectory"} to check acl
604            solrQuery.addFilterQuery(aclFilterQuery.toString());
605        }
606        
607        private void _checkAllowedUsers(SolrQuery solrQuery, AllowedUsers allowedUsersToCompare)
608        {
609            StringBuilder fq = new StringBuilder("{!aclCompare ");
610            if (allowedUsersToCompare.isAnonymousAllowed())
611            {
612                fq.append("anonymous=true");
613            }
614            else
615            {
616                if (allowedUsersToCompare.isAnyConnectedUserAllowed())
617                {
618                    fq.append("anyConnected=true ");
619                }
620                Set<String> allowedUsers = allowedUsersToCompare.getAllowedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet());
621                Set<String> deniedUsers = allowedUsersToCompare.getDeniedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet());
622                Set<String> allowedGroups = allowedUsersToCompare.getAllowedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet());
623                Set<String> deniedGroups = allowedUsersToCompare.getDeniedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet());
624                if (!allowedUsers.isEmpty())
625                {
626                    fq.append("allowedUsers=").append(String.join(",", allowedUsers)).append(" ");
627                }
628                if (!deniedUsers.isEmpty())
629                {
630                    fq.append("deniedUsers=").append(String.join(",", deniedUsers)).append(" ");
631                }
632                if (!allowedGroups.isEmpty())
633                {
634                    fq.append("allowedGroups=").append(String.join(",", allowedGroups)).append(" ");
635                }
636                if (!deniedGroups.isEmpty())
637                {
638                    fq.append("deniedGroups=").append(String.join(",", deniedGroups));
639                }
640            }
641            fq.append("}");
642            solrQuery.addFilterQuery(fq.toString());
643        }
644        
645        /**
646         * Set the sort definition in the solr query object.
647         * @param solrQuery The solr query object.
648         * @param sortCriteria The sort criteria.
649         */
650        protected void setSort(SolrQuery solrQuery, List<Sort> sortCriteria)
651        {
652            if (sortCriteria.isEmpty())
653            {
654                solrQuery.addSort("score", ORDER.desc);
655            }
656            for (Sort sortCriterion : sortCriteria)
657            {
658                solrQuery.addSort(sortCriterion.getField(), sortCriterion.getOrder() == Order.ASC ? ORDER.asc : ORDER.desc);
659            }
660        }
661        
662        /**
663         * Set the facet definition in the solr query object and return a mapping from solr field name to criterion ID.
664         * @param solrQuery the solr query object to fill.
665         * @param facets The facet definitions to use.
666         * @param facetValues the facet values.
667         */
668        protected void setFacets(SolrQuery solrQuery, Collection<SearchField> facets, Map<String, List<String>> facetValues)
669        {
670            if (facets.size() > 0)
671            {
672                // unlimited facet values
673                solrQuery.setFacetLimit(-1);
674                solrQuery.setFacet(true);
675                solrQuery.setFacetMinCount(1);
676            }
677            
678            for (SearchField facetField : facets)
679            {
680                String fieldName = facetField.getName();
681                String solrFieldName = facetField.getFacetField();
682                
683                if (StringUtils.isNotBlank(fieldName))
684                {
685                    if (facetField.isJoined())
686                    {
687                        _setJoinedFacet(solrQuery, fieldName, facetField.getJoinedPaths(), solrFieldName, facetField.getFacetFunction(), facetValues);
688                    }
689                    else
690                    {
691                        _setNonJoinedFacet(solrQuery, fieldName, solrFieldName, facetValues);
692                    }
693                }
694            }
695        }
696        
697        private void _setJoinedFacet(SolrQuery solrQuery, String fieldName, List<String> joinedPaths, String solrFieldName, String facetFunction, Map<String, List<String>> facetValues)
698        {
699            List<String> fieldFacetValues = facetValues.get(fieldName);
700            if (fieldFacetValues != null && !fieldFacetValues.isEmpty())
701            {
702                StringBuilder fq = new StringBuilder();
703                fq.append("{!tag=").append(fieldName).append("}(");
704                
705                int i = 0;
706                fq.append("{!ametys join=").append(StringUtils.join(joinedPaths, "->")).append(" q=\"");
707                for (String facetValue : fieldFacetValues)
708                {
709                    if (i > 0)
710                    {
711                        fq.append(" OR ");
712                    }
713                    fq.append(solrFieldName).append(":\\\"").append(facetValue).append("\\\"");
714                    i++;
715                }
716                fq.append("\"})");
717                solrQuery.addFilterQuery(fq.toString());
718            }
719
720            solrQuery.add("facet.function", "{!ex=" + fieldName + " key=" + fieldName + "}" + facetFunction);
721        }
722        
723        private void _setNonJoinedFacet(SolrQuery solrQuery, String fieldName, String solrFieldName, Map<String, List<String>> facetValues)
724        {
725            List<String> fieldFacetValues = facetValues.get(fieldName);
726            if (fieldFacetValues != null && !fieldFacetValues.isEmpty())
727            {
728                StringBuilder fq = new StringBuilder();
729                fq.append("{!tag=").append(fieldName).append("}(");
730                
731                int i = 0;
732                for (String facetValue : fieldFacetValues)
733                {
734                    if (i > 0)
735                    {
736                        fq.append(" OR ");
737                    }
738                    // TODO escape query chars?
739                    fq.append(solrFieldName).append(":\"").append(facetValue).append('"');
740                    i++;
741                }
742                fq.append(')');
743                solrQuery.addFilterQuery(fq.toString());
744            }
745            
746            String facetFieldDef = "{!ex=" + fieldName + " key=" + fieldName + "}" + solrFieldName;
747            solrQuery.addFacetField(facetFieldDef);
748        }
749        
750        /**
751         * Retrieve the facet results from the solr response.
752         * @param response the solr response.
753         * @param facets The facet fields to return.
754         * @return the facet results.
755         */
756        protected Map<String, Map<String, Integer>> getFacetResults(QueryResponse response, Collection<SearchField> facets)
757        {
758            Map<String, Map<String, Integer>> facetResults = new LinkedHashMap<>();
759            
760            for (SearchField facetField : facets)
761            {
762                String fieldName = facetField.getName();
763                FacetField solrFacetField = response.getFacetField(fieldName);
764                
765                List<Count> values = solrFacetField.getValues();
766                
767                Map<String, Integer> solrFacetValues = new HashMap<>();
768                facetResults.put(fieldName, solrFacetValues);
769                
770                for (Count count : values)
771                {
772                    solrFacetValues.put(count.getName(), (int) count.getCount());
773                }
774            }
775            
776            return facetResults;
777        }
778        
779        /**
780         * Template method to do additional operations on the Solr query before passing it to the Solr client
781         * @param query the Solr query
782         */
783        protected void modifySolrQuery(SolrQuery query)
784        {
785            // do nothing by default
786        }
787    }
788    
789}