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.Map.Entry;
026import java.util.Optional;
027import java.util.stream.Collectors;
028
029import org.apache.solr.client.solrj.SolrQuery;
030import org.apache.solr.client.solrj.response.QueryResponse;
031import org.slf4j.Logger;
032
033import org.ametys.cms.search.SearchField;
034import org.ametys.cms.search.SearchResult;
035import org.ametys.cms.search.SearchResults;
036import org.ametys.cms.search.SearchResultsIterable;
037import org.ametys.cms.search.solr.SearcherFactory;
038import org.ametys.cms.search.solr.SolrSearchResults;
039import org.ametys.plugins.repository.AmetysObject;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041
042/**
043 * {@link SearcherFactory} for creating a searcher for {@link AbstractSearchGenerator}
044 */
045public class FrontOfficeSearcherFactory extends SearcherFactory
046{
047    /** The component role. */
048    @SuppressWarnings("hiding")
049    public static final String ROLE = FrontOfficeSearcherFactory.class.getName();
050    
051    @Override
052    public FrontOfficeSearcher create()
053    {
054        return new FrontOfficeSearcher(getLogger());
055    }
056    
057    /**
058     * Searcher for {@link AbstractSearchGenerator}
059     */
060    public class FrontOfficeSearcher extends Searcher
061    {
062        private Collection<QueryFacet> _queryFacets;
063        private Collection<String> _queryFacetValues;
064        
065        /**
066         * Build a Searcher with default values.
067         * @param logger The logger.
068         */
069        protected FrontOfficeSearcher(Logger logger)
070        {
071            super(logger);
072            _queryFacets = new ArrayList<>();
073            _queryFacetValues = new HashSet<>();
074        }
075        
076        /**
077         * Sets the query facets (facet.query parameters)
078         * @param queryFacets The facets
079         * @return The Searcher object itself.
080         */
081        public FrontOfficeSearcher withQueryFacets(Collection<QueryFacet> queryFacets)
082        {
083            _queryFacets = new ArrayList<>(queryFacets);
084            return this;
085        }
086        
087        /**
088         * Set the facet.query values
089         * @param queryFacetValues The values
090         * @return The Searcher object itself.
091         */
092        public FrontOfficeSearcher withQueryFacetValues(Collection<String> queryFacetValues)
093        {
094            _queryFacetValues = new HashSet<>(queryFacetValues);
095            return this;
096        }
097        
098        @Override
099        protected void modifySolrQuery(SolrQuery query)
100        {
101            if (_queryFacets.size() > 0)
102            {
103                // unlimited facet values
104                query.setFacetLimit(-1);
105                query.setFacet(true);
106            }
107            
108            _queryFacets.forEach(facet ->
109            {
110                String keyName = facet.getKeyName();
111                String excludedTagName = facet.getExcludedTagName();
112                String facetQuery = facet.getFacetQuery();
113                boolean queryFacetValue = _queryFacetValues.contains(keyName);
114                
115                if (queryFacetValue)
116                {
117                    StringBuilder fq = new StringBuilder();
118                    fq.append("{!tag=").append(excludedTagName)
119                      .append("}(")
120                      .append(facetQuery)
121                      .append(')');
122                    query.addFilterQuery(fq.toString());
123                }
124                
125                query.addFacetQuery("{!ex=" + excludedTagName + " key=" + keyName + "}" + facetQuery);
126            });
127        }
128        
129        @Override
130        public <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<SearchField> facets) throws Exception
131        {
132            SearchResults<A> wrappedRes = super._buildResults(response, facets);
133            Collection<String> queryFacetKeys = _queryFacets.stream().map(QueryFacet::getKeyName).collect(Collectors.toList());
134            Map<String, Integer> facetQueryResults = Optional.ofNullable(response.getFacetQuery())
135                                                      .orElseGet(HashMap::new)
136                                                      .entrySet()
137                                                      .stream()
138                                                      .filter(e -> queryFacetKeys.contains(e.getKey()))
139                                                      .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
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}