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.content;
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;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031
032import org.ametys.cms.content.indexing.solr.SolrFieldNames;
033import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
034import org.ametys.cms.contenttype.ContentTypesHelper;
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.search.QueryBuilder;
037import org.ametys.cms.search.SearchResults;
038import org.ametys.cms.search.SortOrder;
039import org.ametys.cms.search.model.SearchModel;
040import org.ametys.cms.search.model.SearchModelHelper;
041import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
042import org.ametys.cms.search.query.DocumentTypeQuery;
043import org.ametys.cms.search.query.Query;
044import org.ametys.cms.search.solr.SearcherFactory;
045import org.ametys.cms.search.solr.SearcherFactory.FacetDefinition;
046import org.ametys.cms.search.solr.SearcherFactory.Searcher;
047import org.ametys.cms.search.solr.SearcherFactory.SortDefinition;
048import org.ametys.plugins.repository.AmetysObject;
049import org.ametys.plugins.repository.AmetysObjectIterable;
050import org.ametys.runtime.plugin.component.AbstractLogEnabled;
051
052/**
053 * Component creating content searchers from {@link SearchModel}s or content type IDs.
054 */
055public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable
056{
057    /** The component role. */
058    public static final String ROLE = ContentSearcherFactory.class.getName();
059    
060    /** The searcher factory. */
061    protected SearcherFactory _searcherFactory;
062    
063    /** The query builder. */
064    protected QueryBuilder _queryBuilder;
065    
066    /** The content type extension point. */
067    protected ContentTypeExtensionPoint _cTypeEP;
068    
069    /** The content type helper. */
070    protected ContentTypesHelper _contentTypesHelper;
071    
072    /** The system property extension point. */
073    protected SystemPropertyExtensionPoint _sysPropEP;
074    
075    /** The search helper. */
076    protected ContentSearchHelper _searchHelper;
077    
078    /** The search model helper */
079    protected SearchModelHelper _searchModelHelper;
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
085        _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE);
086        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
087        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
088        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
089        
090        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
091        _searchModelHelper = (SearchModelHelper) manager.lookup(SearchModelHelper.ROLE);
092    }
093    
094    /**
095     * Create a ContentSearcher from a search model.
096     * @param searchModel The reference search model.
097     * @return a ContentSearcher backed by the given search model.
098     */
099    public SearchModelContentSearcher create(SearchModel searchModel)
100    {
101        return new SearchModelContentSearcher(searchModel);
102    }
103    
104    /**
105     * Create a simple ContentSearcher from a list of content types.
106     * @param contentTypes The content types to search on.
107     * @return a ContentSearcher referencing the given content types.
108     */
109    public SimpleContentSearcher create(String... contentTypes)
110    {
111        return new SimpleContentSearcher(Arrays.asList(contentTypes));
112    }
113    
114    /**
115     * Create a simple ContentSearcher from a list of content types.
116     * @param contentTypes The content types to search on.
117     * @return a ContentSearcher referencing the given content types.
118     */
119    public SimpleContentSearcher create(Collection<String> contentTypes)
120    {
121        return new SimpleContentSearcher(contentTypes);
122    }
123    
124    /**
125     * A ContentSearcher backed by a {@link SearchModel}.
126     */
127    public class SearchModelContentSearcher
128    {
129        private SearchModel _searchModel;
130        private List<ContentSearchSort> _sort;
131        private String _searchMode;
132        private int _start;
133        private int _maxResults;
134        private boolean _checkRights;
135        
136        /**
137         * Build a ContentSearcher referencing a {@link SearchModel}.
138         * @param searchModel the {@link SearchModel}.
139         */
140        public SearchModelContentSearcher(SearchModel searchModel)
141        {
142            this._searchModel = searchModel;
143            this._sort = new ArrayList<>();
144            this._searchMode = "simple";
145            this._start = 0;
146            this._maxResults = Integer.MAX_VALUE;
147            this._checkRights = true;
148        }
149        
150        /**
151         * Add a sort criterion.
152         * @param fieldRef The field reference (name of a SearchField).
153         * @param order The sort order.
154         * @return The ContentSearcher itself.
155         */
156        public SearchModelContentSearcher addSort(String fieldRef, SortOrder order)
157        {
158            _sort.add(new ContentSearchSort(fieldRef, order));
159            return this;
160        }
161        
162        /**
163         * Set the sort criteria.
164         * @param sortCriteria The sort criteria as a List.
165         * @return The ContentSearcher itself.
166         */
167        public SearchModelContentSearcher withSort(List<ContentSearchSort> sortCriteria)
168        {
169            _sort = new ArrayList<>(sortCriteria);
170            return this;
171        }
172        
173        /**
174         * Set the search mode.
175         * @param searchMode The search mode.
176         * @return The ContentSearcher itself.
177         */
178        public SearchModelContentSearcher withSearchMode(String searchMode)
179        {
180            _searchMode = searchMode;
181            return this;
182        }
183        
184        /**
185         * Set the limits to use.
186         * @param start The start index.
187         * @param maxResults The maximum number of results.
188         * @return The ContentSearcher itself.
189         */
190        public SearchModelContentSearcher withLimits(int start, int maxResults)
191        {
192            this._start = start;
193            this._maxResults = maxResults;
194            return this;
195        }
196        
197        /**
198         * Whether to check rights when searching, false otherwise.
199         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
200         * @return The ContentSearcher itself.
201         */
202        public SearchModelContentSearcher setCheckRights(boolean checkRights)
203        {
204            _checkRights = checkRights;
205            return this;
206        }
207        
208        /**
209         * Search the contents.
210         * @param values The values for criteria defined in the model.
211         * @param <C> The type Content
212         * @return The search results as {@link AmetysObject}s.
213         * @throws Exception if an error occurs.
214         */
215        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception
216        {
217            return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search();
218        }
219        
220        /**
221         * Search the contents.
222         * @param values The values for criteria defined in the model.
223         * @param contextualParameters The search contextual parameters.
224         * @param <C> The type Content
225         * @return The search results as {@link AmetysObject}s.
226         * @throws Exception if an error occurs.
227         */
228        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
229        {
230            return _searcher(values, Collections.emptyMap(), contextualParameters).search();
231        }
232        
233        /**
234         * Search the contents.
235         * @param values The values for criteria defined in the model.
236         * @param <C> The type Content
237         * @return The search results.
238         * @throws Exception if an error occurs.
239         */
240        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception
241        {
242            return searchWithFacets(values, Collections.emptyMap());
243        }
244        
245        /**
246         * Search the contents.
247         * @param <C> The type Content
248         * @param values The values for criteria defined in the model.
249         * @param contextualParameters The search contextual parameters.
250         * @return The search results.
251         * @throws Exception if an error occurs.
252         */
253        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
254        {
255            return searchWithFacets(values, Collections.emptyMap(), contextualParameters);
256        }
257        
258        /**
259         * Search the contents.
260         * @param <C> The type Content
261         * @param values The values for criteria defined in the model.
262         * @param facetValues The facet values, indexed 
263         * @param contextualParameters The search contextual parameters. 
264         * @return The search results.
265         * @throws Exception if an error occurs.
266         */
267        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception
268        {
269            return _searcher(values, facetValues, contextualParameters).searchWithFacets();
270        }
271        
272        private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters)
273        {
274            Query query = _queryBuilder.build(_searchModel, _searchMode, values, contextualParameters);
275            
276            Set<String> contentTypeIds = _searchModel.getContentTypes(contextualParameters);
277            List<SortDefinition> sort = _searchHelper.transformContentSearcherSorts(_sort, contentTypeIds);
278            
279            List<FacetDefinition> facets = _searchHelper.getFacetDefinitions(_searchModel, contextualParameters);
280            
281            return _searcherFactory.create()
282                                    .withQuery(query)
283                                    .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
284                                    .withSort(sort)
285                                    .withFacets(facets)
286                                    .withFacetValues(facetValues)
287                                    .withLimits(_start, _maxResults)
288                                    .setCheckRights(_checkRights);
289        }
290    }
291    
292    /**
293     * A ContentSearcher on a list of content types.
294     */
295    public class SimpleContentSearcher
296    {
297        
298        private Set<String> _contentTypeIds;
299        private List<ContentSearchSort> _sort;
300        private List<String> _facets;
301        private int _start;
302        private int _maxResults;
303        private boolean _checkRights;
304        private List<String> _filterQueryStrings;
305        private List<Query> _filterQueries;
306        
307        /**
308         * Build a content searcher on a list of content types.
309         * @param contentTypes A collection of content types to search on.
310         */
311        public SimpleContentSearcher(Collection<String> contentTypes)
312        {
313            this._contentTypeIds = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
314            this._sort = new ArrayList<>();
315            this._facets = new ArrayList<>();
316            this._start = 0;
317            this._maxResults = Integer.MAX_VALUE;
318            this._checkRights = true;
319        }
320        
321        /**
322         * Set the filter queries.
323         * @param filterQueries the filter queries.
324         * @return The ContentSearcher itself.
325         */
326        public SimpleContentSearcher withFilterQueries(List<Query> filterQueries)
327        {
328            _filterQueries = filterQueries;
329            return this;
330        }
331        
332        /**
333         * Set the filter queries.
334         * @param filterQueryStrings the filter queries.
335         * @return The ContentSearcher itself.
336         */
337        public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings)
338        {
339            _filterQueryStrings = filterQueryStrings;
340            return this;
341        }
342        
343        /**
344         * Set the sort criteria.
345         * @param sortCriteria The sort criteria as a List.
346         * @return The ContentSearcher itself.
347         */
348        public SimpleContentSearcher withSort(List<ContentSearchSort> sortCriteria)
349        {
350            _sort = new ArrayList<>(sortCriteria);
351            return this;
352        }
353        
354        /**
355         * Add a sort criterion.
356         * @param fieldRef The field reference (name of a SearchField).
357         * @param order The sort order.
358         * @return The ContentSearcher itself.
359         */
360        public SimpleContentSearcher addSort(String fieldRef, SortOrder order)
361        {
362            _sort.add(new ContentSearchSort(fieldRef, order));
363            return this;
364        }
365        
366        /**
367         * Set the facets.
368         * @param facets The facets list.
369         * @return The ContentSearcher itself.
370         */
371        public SimpleContentSearcher withFacets(Collection<String> facets)
372        {
373            _facets = new ArrayList<>(facets);
374            return this;
375        }
376        
377        /**
378         * Set the facets.
379         * @param facets The facets list.
380         * @return The ContentSearcher itself.
381         */
382        public SimpleContentSearcher withFacets(String... facets)
383        {
384            _facets = Arrays.asList(facets);
385            return this;
386        }
387        
388        /**
389         * Set the limits to use.
390         * @param start The start index.
391         * @param maxResults The maximum number of results.
392         * @return The ContentSearcher itself.
393         */
394        public SimpleContentSearcher withLimits(int start, int maxResults)
395        {
396            this._start = start;
397            this._maxResults = maxResults;
398            return this;
399        }
400        
401        /**
402         * Whether to check rights when searching, false otherwise.
403         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
404         * @return The ContentSearcher itself.
405         */
406        public SimpleContentSearcher setCheckRights(boolean checkRights)
407        {
408            _checkRights = checkRights;
409            return this;
410        }
411        
412        /**
413         * Search the contents.
414         * @param <C> The type Content
415         * @param query The query object to execute.
416         * @return The search results as {@link AmetysObject}s.
417         * @throws Exception if an error occurs.
418         */
419        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
420        {
421            return _searcher(query, Collections.emptyMap()).search();
422        }
423        
424        /**
425         * Search the contents.
426         * @param <C> The type Content
427         * @param query The query string to execute.
428         * @return The search results as {@link AmetysObject}s.
429         * @throws Exception if an error occurs.
430         */
431        public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception
432        {
433            return _searcher(query, Collections.emptyMap()).search();
434        }
435        
436        /**
437         * Search the contents.
438         * @param <C> The type Content
439         * @param query The query objet to execute.
440         * @return The search results.
441         * @throws Exception if an error occurs.
442         */
443        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
444        {
445            return searchWithFacets(query, Collections.emptyMap());
446        }
447        
448        /**
449         * Search the contents.
450         * @param <C> The type Content
451         * @param query The query string to execute.
452         * @return The search results.
453         * @throws Exception if an error occurs.
454         */
455        public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception
456        {
457            return searchWithFacets(query, Collections.emptyMap());
458        }
459        
460        /**
461         * Search the contents.
462         * @param <C> The type Content
463         * @param query The query object to execute.
464         * @param facetValues The facet values.
465         * @return The search results.
466         * @throws Exception if an error occurs.
467         */
468        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
469        {
470            return _searcher(query, facetValues).searchWithFacets();
471        }
472        
473        /**
474         * Search the contents.
475         * @param <C> The type Content
476         * @param query The query string to execute.
477         * @param facetValues The facet values.
478         * @return The search results.
479         * @throws Exception if an error occurs.
480         */
481        public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception
482        {
483            return _searcher(query, facetValues).searchWithFacets();
484        }
485        
486        private Searcher _searcher(String query, Map<String, List<String>> facetValues)
487        {
488            return _searcher(facetValues).withQueryString(query);
489        }
490        
491        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
492        {
493            return _searcher(facetValues).withQuery(query);
494        }
495        
496        private Searcher _searcher(Map<String, List<String>> facetValues)
497        {
498            Set<String> commonContentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypeIds);
499            List<SortDefinition> sort = _searchHelper.transformContentSearcherSorts(_sort, commonContentTypeIds);
500            List<FacetDefinition> facets = _searchHelper.getFacetDefinitions(_facets, commonContentTypeIds);
501            
502            List<Query> filterQueries = new ArrayList<>();
503            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
504            
505            if (!_contentTypeIds.isEmpty())
506            {
507                filterQueries.add(_searchModelHelper.createContentTypeOrMixinQuery(_contentTypeIds));
508            }
509            
510            if (_filterQueries != null)
511            {
512                filterQueries.addAll(_filterQueries);
513            }
514            
515            List<String> filterQueryStrings = new ArrayList<>();
516            
517            if (_filterQueryStrings != null)
518            {
519                filterQueryStrings.addAll(_filterQueryStrings);
520            }
521            
522            return _searcherFactory.create()
523                                   .withFilterQueries(filterQueries)
524                                   .withFilterQueryStrings(filterQueryStrings)
525                                   .withSort(sort)
526                                   .withFacets(facets)
527                                   .withFacetValues(facetValues)
528                                   .withLimits(_start, _maxResults)
529                                   .setCheckRights(_checkRights);
530        }
531    }
532    
533    /**
534     * Record representing a sort criterion.
535     * @param sortField The sort field. Can be a path to an element
536     * @param order The sort order
537     */
538    public record ContentSearchSort (String sortField, SortOrder order) { /* empty */ }
539}