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