001/*
002 *  Copyright 2017 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.web.frontoffice;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.stream.Collectors;
027
028import org.apache.solr.client.solrj.request.json.DomainMap;
029import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
030import org.apache.solr.client.solrj.request.json.QueryFacetMap;
031import org.apache.solr.client.solrj.response.QueryResponse;
032import org.apache.solr.client.solrj.response.json.NestableJsonFacet;
033import org.slf4j.Logger;
034
035import org.ametys.cms.search.SearchResult;
036import org.ametys.cms.search.SearchResults;
037import org.ametys.cms.search.SearchResultsIterable;
038import org.ametys.cms.search.solr.SearcherFactory;
039import org.ametys.cms.search.solr.SolrSearchResults;
040import org.ametys.plugins.repository.AmetysObject;
041import org.ametys.plugins.repository.AmetysObjectIterable;
042
043/**
044 * {@link SearcherFactory} for creating a searcher for {@link AbstractSearchGenerator}
045 */
046public class FrontOfficeSearcherFactory extends SearcherFactory
047{
048    /** The component role. */
049    @SuppressWarnings("hiding")
050    public static final String ROLE = FrontOfficeSearcherFactory.class.getName();
051    
052    @Override
053    public FrontOfficeSearcher create()
054    {
055        return new FrontOfficeSearcher(getLogger());
056    }
057    
058    /**
059     * Searcher for {@link AbstractSearchGenerator}
060     */
061    public class FrontOfficeSearcher extends Searcher
062    {
063        private Collection<QueryFacet> _queryFacets;
064        private Collection<String> _queryFacetValues;
065        
066        /**
067         * Build a Searcher with default values.
068         * @param logger The logger.
069         */
070        protected FrontOfficeSearcher(Logger logger)
071        {
072            super(logger);
073            _queryFacets = new ArrayList<>();
074            _queryFacetValues = new HashSet<>();
075        }
076        
077        /**
078         * Sets the query facets (facet.query parameters)
079         * @param queryFacets The facets
080         * @return The Searcher object itself.
081         */
082        public FrontOfficeSearcher withQueryFacets(Collection<QueryFacet> queryFacets)
083        {
084            _queryFacets = new ArrayList<>(queryFacets);
085            return this;
086        }
087        
088        /**
089         * Set the facet.query values
090         * @param queryFacetValues The values
091         * @return The Searcher object itself.
092         */
093        public FrontOfficeSearcher withQueryFacetValues(Collection<String> queryFacetValues)
094        {
095            _queryFacetValues = new HashSet<>(queryFacetValues);
096            return this;
097        }
098        
099        @Override
100        protected void modifySolrQuery(JsonQueryRequest solrQuery)
101        {
102            _queryFacets.forEach(facet ->
103            {
104                String keyName = facet.getKeyName();
105                String excludedTagName = facet.getExcludedTagName();
106                String facetQuery = facet.getFacetQuery();
107
108                boolean queryFacetValue = _queryFacetValues.contains(keyName);
109                
110                if (queryFacetValue)
111                {
112                    solrQuery.withFilter(Map.of("#" + excludedTagName, facetQuery));
113                }
114                
115                QueryFacetMap facetMap = new QueryFacetMap(facetQuery).withDomain(new DomainMap().withTagsToExclude(excludedTagName));
116                
117                solrQuery.withFacet(keyName, facetMap);
118            });
119        }
120        
121        @Override
122        public <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<FacetDefinition> facets) throws Exception
123        {
124            SearchResults<A> wrappedRes = super._buildResults(response, facets);
125            Collection<String> queryFacetKeys = _queryFacets.stream().map(QueryFacet::getKeyName).collect(Collectors.toList());
126            
127            Map<String, Integer> facetQueryResults = new HashMap<>();
128            NestableJsonFacet facetingResponse = response.getJsonFacetingResponse();
129            if (facetingResponse != null)
130            {
131                for (String query : facetingResponse.getQueryFacetNames())
132                {
133                    if (queryFacetKeys.contains(query))
134                    {
135                        facetQueryResults.put(query, (int) facetingResponse.getQueryFacet(query).getCount());
136                    }
137                }
138            }
139
140            return new FrontOfficeSolrSearchResults<>(wrappedRes, response, facetQueryResults);
141        }
142    }
143    
144    /**
145     * Wraps a 'facet.query' parameter
146     */
147    public static class QueryFacet
148    {
149        private final String _keyName;
150        private final String _excludedTagName;
151        private final String _facetQuery;
152
153        /**
154         * Creates a QueryFacet
155         * @param keyName The name of the query facet
156         * @param excludedTagName The tag for exclusion for facet counting
157         * @param facetQuery The value of the query
158         */
159        public QueryFacet(String keyName, String excludedTagName, String facetQuery)
160        {
161            _keyName = keyName;
162            _excludedTagName = excludedTagName;
163            _facetQuery = facetQuery;
164        }
165
166        /**
167         * Gets the keyName
168         * @return the keyName
169         */
170        public String getKeyName()
171        {
172            return _keyName;
173        }
174
175        /**
176         * Gets the excludedTagName
177         * @return the excludedTagName
178         */
179        public String getExcludedTagName()
180        {
181            return _excludedTagName;
182        }
183
184        /**
185         * Gets the facetQuery
186         * @return the _acetQuery
187         */
188        public String getFacetQuery()
189        {
190            return _facetQuery;
191        }
192    }
193    
194    /**
195     * Wrapper of a {@link SolrSearchResults} for {@link AbstractSearchGenerator} (add the possibility to add facet.queries)
196     * @param <A> the actual type of {@link AmetysObject}s. 
197     */
198    public static class FrontOfficeSolrSearchResults<A extends AmetysObject>/* extends SolrSearchResults<A>*/implements SearchResults<A>
199    {
200        private Map<String, Integer> _facetQueryResults;
201        private SearchResults<A> _searchResults;
202        
203        /**
204         * Build a FrontOfficeSolrSearchResults object.
205         * @param searchResults The wrapped {@link SearchResults} object
206         * @param response the solr search response.
207         * @param facetQueryResults The facet.query results.
208         */
209        public FrontOfficeSolrSearchResults(SearchResults<A> searchResults, 
210                                            QueryResponse response, 
211                                            Map<String, Integer> facetQueryResults)
212        {
213            _searchResults = searchResults;
214            _facetQueryResults = facetQueryResults;
215        }
216        
217        @Override
218        public SearchResultsIterable<SearchResult<A>> getResults()
219        {
220            return _searchResults.getResults();
221        }
222        
223        @Override
224        public AmetysObjectIterable<A> getObjects()
225        {
226            return _searchResults.getObjects();
227        }
228        
229        @Override
230        public Iterable<String> getObjectIds()
231        {
232            return _searchResults.getObjectIds();
233        }
234        
235        @Override
236        public Map<String, Map<String, Integer>> getFacetResults()
237        {
238            return _searchResults.getFacetResults();
239        }
240        
241        /**
242         * Gets the facet.query results as a Map
243         * @return the facet.query results
244         */
245        public Map<String, Integer> getFacetQueryResults()
246        {
247            return Collections.unmodifiableMap(_facetQueryResults);
248        }
249        
250        @Override
251        public long getTotalCount()
252        {
253            return _searchResults.getTotalCount();
254        }
255        
256        @Override
257        public float getMaxScore()
258        {
259            return _searchResults.getMaxScore();
260        }
261        
262        @Override
263        public Optional<Map<String, Object>> getDebugMap()
264        {
265            return _searchResults.getDebugMap();
266        }
267    }
268}