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