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.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.container.ContainerUtil;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.environment.Request;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.contenttype.ContentTypesHelper;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.SearchResults;
038import org.ametys.cms.search.Sort;
039import org.ametys.cms.search.cocoon.SearchAction;
040import org.ametys.cms.search.content.ContentSearcherFactory.SimpleContentSearcher;
041import org.ametys.cms.search.model.SearchCriterion;
042import org.ametys.cms.search.model.SearchModel;
043import org.ametys.cms.search.query.QuerySyntaxException;
044import org.ametys.cms.search.ui.model.ColumnHelper;
045import org.ametys.cms.search.ui.model.ColumnHelper.Column;
046import org.ametys.core.util.AvalonLoggerAdapter;
047
048/**
049 * Execute a solr query with custom columns and facets.
050 */
051public class SolrQuerySearchAction extends SearchAction
052{
053    /** The content types helper */
054    protected ContentTypesHelper _contentTypesHelper;
055    
056    /** The helper for columns */
057    protected ColumnHelper _columnHelper;
058    
059    @Override
060    public void service(ServiceManager serviceManager) throws ServiceException
061    {
062        super.service(serviceManager);
063        
064        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
065        _columnHelper = (ColumnHelper) serviceManager.lookup(ColumnHelper.ROLE);
066    }
067    
068    @Override
069    protected void doSearch(Request request, SearchModel model, int offset, int maxResults, Map<String, Object> jsParameters, Map<String, Object> contextualParameters) throws Exception
070    {
071        SearchValues searchValues = getSearchValues(jsParameters);
072        
073        CriteriaSearchModelWrapper modelWrapper = new CriteriaSearchModelWrapper(model, manager, _context, new AvalonLoggerAdapter(getLogger()));
074        ContainerUtil.service(modelWrapper, manager);
075
076        modelWrapper.setResultColumns(searchValues.getBaseContentTypes(), searchValues.getColumns(), contextualParameters);
077        
078        Map<String, ? extends SearchCriterion> criteria = model.getCriteria(contextualParameters);
079        String lang = _queryBuilder.getCriteriaLanguage(criteria, null, searchValues.getValues(), contextualParameters);
080        if (StringUtils.isNotEmpty(lang))
081        {
082            request.setAttribute(SEARCH_LOCALE, new Locale(lang));
083        }
084        
085        Collection<String> transformedFacets = modelWrapper.setFacetedCriteria(searchValues.getBaseContentTypes(), searchValues.getFacets(), contextualParameters);
086        List<Sort> sorts = getSort(searchValues.getSortInfo(), searchValues.getGroupInfo());
087        
088        SearchResults<Content> results = getResults(searchValues, offset, maxResults, transformedFacets, sorts);
089        
090        request.setAttribute(SEARCH_RESULTS, results);
091        request.setAttribute(SEARCH_MODEL, modelWrapper);
092    }
093    
094    /**
095     * Get the object representing search values from the JS parameters.
096     * @param jsParameters The JS parameters
097     * @return an object representing the search values
098     */
099    protected SearchValues getSearchValues(Map<String, Object> jsParameters)
100    {
101        return new SearchValues(jsParameters);
102    }
103    
104    /**
105     * Get the query string from the search values.
106     * @param searchValues The search values
107     * @return the query string
108     * @throws QuerySyntaxException if an error occurs
109     */
110    protected String getQueryString(SearchValues searchValues) throws QuerySyntaxException
111    {
112        return SolrContentQueryHelper.buildQuery(_queryBuilder, searchValues.getBaseQuery(), Collections.EMPTY_SET/*content types or mixin query will be handled by the SimpleContentSearcher*/, searchValues.getWorkflowSteps());
113    }
114    
115    /**
116     * Create the searcher and execute it from the search values.
117     * @param searchValues The search values
118     * @param offset The offset
119     * @param maxResults The max number of results
120     * @param facets Facets renamed for Solr
121     * @param sorts The sorts
122     * @return the search results
123     * @throws Exception if an error occurs
124     */
125    protected SearchResults<Content> getResults(SearchValues searchValues, int offset, int maxResults, Collection<String> facets, List<Sort> sorts) throws Exception
126    {
127        return getContentSearcher(searchValues, offset, maxResults, facets, sorts)
128                .searchWithFacets(getQueryString(searchValues), searchValues.getFacetValues());
129    }
130    
131    /**
132     * Get the content search from the search values
133     * @param searchValues The search values
134     * @param offset The offset
135     * @param maxResults The max number of results
136     * @param facets Facets renamed for Solr
137     * @param sorts The sorts
138     * @return the content searcher
139     */
140    protected SimpleContentSearcher getContentSearcher(SearchValues searchValues, int offset, int maxResults, Collection<String> facets, List<Sort> sorts)
141    {
142        return _searcherFactory.create(searchValues.getContentTypes())
143                .withSort(sorts)
144                .withFacets(facets)
145                .withLimits(offset, maxResults);
146    }
147    
148    /**
149     * Object representing search values.
150     */
151    @SuppressWarnings("unchecked")
152    protected class SearchValues
153    {
154        /** The JS parameters */
155        protected Map<String, Object> _jsParameters;
156        /** The values from JS parameters */
157        protected Map<String, Object> _values;
158        /** The base query */
159        protected String _baseQuery;
160        /** The content types */
161        protected Set<String> _contentTypeIds;
162        /** The base content types (common content types) */
163        protected Set<String> _baseContentTypeIds;
164        /** The facet fields */
165        protected Collection<String> _facets;
166        /** The facet values */
167        protected Map<String, List<String>> _facetValues;
168        /** The columns */
169        protected Collection<Column> _columns;
170        /** The sorts */
171        protected String _sortInfo;
172        /** The groups */
173        protected String _groupInfo;
174        /** The workflow steps */
175        protected Set<Integer> _wfSteps;
176        
177        /**
178         * Constructor to build the object from JS parameters.
179         * @param jsParameters The JS parameters
180         */
181        protected SearchValues(Map<String, Object> jsParameters)
182        {
183            _jsParameters = jsParameters;
184            _parseValues();
185            _parseContentTypes();
186            _parseQuery();
187            _parseFacets();
188            _parseFacetValues();
189            _parseColumns();
190            _parseSortInfo();
191            _parseGroupInfo();
192            _parseWorkflowSteps();
193        }
194        
195        private void _parseValues()
196        {
197            _values = (Map<String, Object>) _jsParameters.get("values");
198        }
199        
200        private void _parseContentTypes()
201        {
202            _contentTypeIds = SolrContentQueryHelper.getContentTypes(_jsParameters);
203            _baseContentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypeIds);
204        }
205        
206        private void _parseQuery()
207        {
208            _baseQuery = (String) _values.get("query");
209        }
210        
211        private void _parseFacets()
212        {
213            String facetObj = StringUtils.defaultString((String) _values.get("facets"));
214            _facets = Arrays.asList(StringUtils.split(facetObj, ", ")).stream().map(s -> s.replaceAll("\\.", "/")).collect(Collectors.toList());
215        }
216        
217        private void _parseFacetValues()
218        {
219            _facetValues = (Map<String, List<String>>) _jsParameters.get("facetValues");
220            if (_facetValues == null)
221            {
222                _facetValues = Collections.emptyMap();
223            }
224        }
225        
226        private void _parseColumns()
227        {
228            Object columnsObject = _values.get("columns");
229            if (columnsObject == null)
230            {
231                // Empty list, but not immutable
232                _columns = new ArrayList();
233            }
234            else if (columnsObject instanceof String)
235            {
236                _columns = _columnHelper.getColumns((String) columnsObject, _baseContentTypeIds);
237            }
238            else if (columnsObject instanceof List)
239            {
240                _columns = _columnHelper.getColumns((List) columnsObject, _baseContentTypeIds);
241            }
242        }
243        
244        private void _parseSortInfo()
245        {
246            _sortInfo = (String) _jsParameters.get("sort");
247        }
248        
249        private void _parseGroupInfo()
250        {
251            _groupInfo = (String) _jsParameters.get("group");
252        }
253        
254        private void _parseWorkflowSteps()
255        {
256            Object wfStepsObj = _values.get("workflowSteps");
257            _wfSteps = new HashSet<>();
258            if (wfStepsObj != null && wfStepsObj instanceof List<?>)
259            {
260                for (String wfStepObj : (List<String>) wfStepsObj)
261                {
262                    if (StringUtils.isNotEmpty(wfStepObj))
263                    {
264                        _wfSteps.add(Integer.parseInt(wfStepObj));
265                    }
266                }
267            }
268        }
269        
270        /**
271         * Get the base query.
272         * @return the base query
273         */
274        protected String getBaseQuery()
275        {
276            return _baseQuery;
277        }
278        
279        /**
280         * Get the columns.
281         * @return the columns
282         */
283        protected Collection<Column> getColumns()
284        {
285            return _columns;
286        }
287        
288        /**
289         * Get the base content types (extract from content types, it's the common ancestors).
290         * @return the base content types
291         */
292        protected Set<String> getBaseContentTypes()
293        {
294            return _baseContentTypeIds;
295        }
296        
297        /**
298         * Get the content types.
299         * @return the content types
300         */
301        protected Set<String> getContentTypes()
302        {
303            return _contentTypeIds;
304        }
305        
306        /**
307         * Get the workflow steps.
308         * @return the workflow steps
309         */
310        protected Set<Integer> getWorkflowSteps()
311        {
312            return _wfSteps;
313        }
314        
315        /**
316         * Get the sort info.
317         * @return the sort info
318         */
319        protected String getSortInfo()
320        {
321            return _sortInfo;
322        }
323        
324        /**
325         * Get the group info.
326         * @return the group info
327         */
328        protected String getGroupInfo()
329        {
330            return _groupInfo;
331        }
332
333        /**
334         * Get the facet fields.
335         * @return the facet fields
336         */
337        protected Collection<String> getFacets()
338        {
339            return _facets;
340        }
341        
342        /**
343         * Get the facet values.
344         * @return the facet values
345         */
346        protected Map<String, List<String>> getFacetValues()
347        {
348            return _facetValues;
349        }
350        
351        /**
352         * Get the values from the JS parameters.
353         * @return the values
354         */
355        protected Map<String, Object> getValues()
356        {
357            return _values;
358        }
359    }
360}