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