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