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