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