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        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
090        _searchModelHelper = (SearchModelHelper) manager.lookup(SearchModelHelper.ROLE);
091    }
092    
093    /**
094     * Create a ContentSearcher from a search model.
095     * @param searchModel The reference search model.
096     * @return a ContentSearcher backed by the given search model.
097     */
098    public SearchModelContentSearcher create(SearchModel searchModel)
099    {
100        return new SearchModelContentSearcher(searchModel);
101    }
102    
103    /**
104     * Create a simple ContentSearcher from a list of content types.
105     * @param contentTypes The content types to search on.
106     * @return a ContentSearcher referencing the given content types.
107     */
108    public SimpleContentSearcher create(String... contentTypes)
109    {
110        return new SimpleContentSearcher(Arrays.asList(contentTypes));
111    }
112    
113    /**
114     * Create a simple ContentSearcher from a list of content types.
115     * @param contentTypes The content types to search on.
116     * @return a ContentSearcher referencing the given content types.
117     */
118    public SimpleContentSearcher create(Collection<String> contentTypes)
119    {
120        return new SimpleContentSearcher(contentTypes);
121    }
122    
123    /**
124     * A ContentSearcher backed by a {@link SearchModel}.
125     */
126    public class SearchModelContentSearcher
127    {
128        private SearchModel _searchModel;
129        private List<ContentSearchSort> _sort;
130        private String _searchMode;
131        private int _start;
132        private int _maxResults;
133        private boolean _checkRights;
134        private Query _highlightingQuery;
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         * Set the highlight to use.
199         * @param query the highlighting query
200         * @return The ContentSearcher itself.
201         */
202        public SearchModelContentSearcher withHighlightingQuery(Query query)
203        {
204            this._highlightingQuery = query;
205            return this;
206        }
207        
208        /**
209         * Whether to check rights when searching, false otherwise.
210         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
211         * @return The ContentSearcher itself.
212         */
213        public SearchModelContentSearcher setCheckRights(boolean checkRights)
214        {
215            _checkRights = checkRights;
216            return this;
217        }
218        
219        /**
220         * Search the contents.
221         * @param values The values for criteria defined in the model.
222         * @param <C> The type Content
223         * @return The search results as {@link AmetysObject}s.
224         * @throws Exception if an error occurs.
225         */
226        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception
227        {
228            return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search();
229        }
230        
231        /**
232         * Search the contents.
233         * @param values The values for criteria defined in the model.
234         * @param contextualParameters The search contextual parameters.
235         * @param <C> The type Content
236         * @return The search results as {@link AmetysObject}s.
237         * @throws Exception if an error occurs.
238         */
239        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
240        {
241            return _searcher(values, Collections.emptyMap(), contextualParameters).search();
242        }
243        
244        /**
245         * Search the contents.
246         * @param values The values for criteria defined in the model.
247         * @param <C> The type Content
248         * @return The search results.
249         * @throws Exception if an error occurs.
250         */
251        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception
252        {
253            return searchWithFacets(values, Collections.emptyMap());
254        }
255        
256        /**
257         * Search the contents.
258         * @param <C> The type Content
259         * @param values The values for criteria defined in the model.
260         * @param contextualParameters The search contextual parameters.
261         * @return The search results.
262         * @throws Exception if an error occurs.
263         */
264        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
265        {
266            return searchWithFacets(values, Collections.emptyMap(), contextualParameters);
267        }
268        
269        /**
270         * Search the contents.
271         * @param <C> The type Content
272         * @param values The values for criteria defined in the model.
273         * @param facetValues The facet values, indexed
274         * @param contextualParameters The search contextual parameters.
275         * @return The search results.
276         * @throws Exception if an error occurs.
277         */
278        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception
279        {
280            return _searcher(values, facetValues, contextualParameters).searchWithFacets();
281        }
282        
283        private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters)
284        {
285            Query query = _queryBuilder.build(_searchModel, _searchMode, values, contextualParameters);
286            
287            Set<String> contentTypeIds = _searchModel.getContentTypes(contextualParameters);
288            List<SortDefinition> sort = _searchHelper.transformContentSearcherSorts(_sort, contentTypeIds);
289            
290            List<FacetDefinition> facets = _searchHelper.getFacetDefinitions(_searchModel, contextualParameters);
291            
292            Searcher searcher = _searcherFactory.create()
293                                    .withQuery(query)
294                                    .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
295                                    .withSort(sort)
296                                    .withFacets(facets)
297                                    .withFacetValues(facetValues)
298                                    .withLimits(_start, _maxResults)
299                                    .setCheckRights(_checkRights)
300                                    .withHighlightingQuery(_highlightingQuery);
301            
302            return searcher;
303        }
304    }
305    
306    /**
307     * A ContentSearcher on a list of content types.
308     */
309    public class SimpleContentSearcher
310    {
311        
312        private Set<String> _contentTypeIds;
313        private List<ContentSearchSort> _sort;
314        private List<String> _facets;
315        private int _start;
316        private int _maxResults;
317        private boolean _checkRights;
318        private List<String> _filterQueryStrings;
319        private List<Query> _filterQueries;
320        private Query _highlightingQuery;
321        
322        /**
323         * Build a content searcher on a list of content types.
324         * @param contentTypes A collection of content types to search on.
325         */
326        public SimpleContentSearcher(Collection<String> contentTypes)
327        {
328            this._contentTypeIds = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
329            this._sort = new ArrayList<>();
330            this._facets = new ArrayList<>();
331            this._start = 0;
332            this._maxResults = Integer.MAX_VALUE;
333            this._checkRights = true;
334        }
335        
336        /**
337         * Set the filter queries.
338         * @param filterQueries the filter queries.
339         * @return The ContentSearcher itself.
340         */
341        public SimpleContentSearcher withFilterQueries(List<Query> filterQueries)
342        {
343            _filterQueries = filterQueries;
344            return this;
345        }
346        
347        /**
348         * Set the filter queries.
349         * @param filterQueryStrings the filter queries.
350         * @return The ContentSearcher itself.
351         */
352        public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings)
353        {
354            _filterQueryStrings = filterQueryStrings;
355            return this;
356        }
357        
358        /**
359         * Set the sort criteria.
360         * @param sortCriteria The sort criteria as a List.
361         * @return The ContentSearcher itself.
362         */
363        public SimpleContentSearcher withSort(List<ContentSearchSort> sortCriteria)
364        {
365            _sort = new ArrayList<>(sortCriteria);
366            return this;
367        }
368        
369        /**
370         * Add a sort criterion.
371         * @param fieldRef The field reference (name of a SearchField).
372         * @param order The sort order.
373         * @return The ContentSearcher itself.
374         */
375        public SimpleContentSearcher addSort(String fieldRef, SortOrder order)
376        {
377            _sort.add(new ContentSearchSort(fieldRef, order));
378            return this;
379        }
380        
381        /**
382         * Set the facets.
383         * @param facets The facets list.
384         * @return The ContentSearcher itself.
385         */
386        public SimpleContentSearcher withFacets(Collection<String> facets)
387        {
388            _facets = new ArrayList<>(facets);
389            return this;
390        }
391        
392        /**
393         * Set the facets.
394         * @param facets The facets list.
395         * @return The ContentSearcher itself.
396         */
397        public SimpleContentSearcher withFacets(String... facets)
398        {
399            _facets = Arrays.asList(facets);
400            return this;
401        }
402        
403        /**
404         * Set the limits to use.
405         * @param start The start index.
406         * @param maxResults The maximum number of results.
407         * @return The ContentSearcher itself.
408         */
409        public SimpleContentSearcher withLimits(int start, int maxResults)
410        {
411            this._start = start;
412            this._maxResults = maxResults;
413            return this;
414        }
415        
416        /**
417         * Set the highlight to use.
418         * @param query the highlighting query
419         * @return The ContentSearcher itself.
420         */
421        public SimpleContentSearcher withHighlightingQuery(Query query)
422        {
423            this._highlightingQuery = query;
424            return this;
425        }
426        
427        /**
428         * Whether to check rights when searching, false otherwise.
429         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
430         * @return The ContentSearcher itself.
431         */
432        public SimpleContentSearcher setCheckRights(boolean checkRights)
433        {
434            _checkRights = checkRights;
435            return this;
436        }
437        
438        /**
439         * Search the contents.
440         * @param <C> The type Content
441         * @param query The query object to execute.
442         * @return The search results as {@link AmetysObject}s.
443         * @throws Exception if an error occurs.
444         */
445        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
446        {
447            return _searcher(query, Collections.emptyMap()).search();
448        }
449        
450        /**
451         * Search the contents.
452         * @param <C> The type Content
453         * @param query The query string to execute.
454         * @return The search results as {@link AmetysObject}s.
455         * @throws Exception if an error occurs.
456         */
457        public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception
458        {
459            return _searcher(query, Collections.emptyMap()).search();
460        }
461        
462        /**
463         * Search the contents.
464         * @param <C> The type Content
465         * @param query The query object to execute.
466         * @return The search results.
467         * @throws Exception if an error occurs.
468         */
469        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
470        {
471            return searchWithFacets(query, Collections.emptyMap());
472        }
473        
474        /**
475         * Search the contents.
476         * @param <C> The type Content
477         * @param query The query string to execute.
478         * @return The search results.
479         * @throws Exception if an error occurs.
480         */
481        public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception
482        {
483            return searchWithFacets(query, Collections.emptyMap());
484        }
485        
486        /**
487         * Search the contents.
488         * @param <C> The type Content
489         * @param query The query object to execute.
490         * @param facetValues The facet values.
491         * @return The search results.
492         * @throws Exception if an error occurs.
493         */
494        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
495        {
496            return _searcher(query, facetValues).searchWithFacets();
497        }
498        
499        /**
500         * Search the contents.
501         * @param <C> The type Content
502         * @param query The query string to execute.
503         * @param facetValues The facet values.
504         * @return The search results.
505         * @throws Exception if an error occurs.
506         */
507        public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception
508        {
509            return _searcher(query, facetValues).searchWithFacets();
510        }
511        
512        private Searcher _searcher(String query, Map<String, List<String>> facetValues)
513        {
514            return _searcher(facetValues).withQueryString(query);
515        }
516        
517        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
518        {
519            return _searcher(facetValues).withQuery(query);
520        }
521        
522        private Searcher _searcher(Map<String, List<String>> facetValues)
523        {
524            Set<String> commonContentTypeIds = _contentTypesHelper.getCommonAncestors(_contentTypeIds);
525            List<SortDefinition> sort = _searchHelper.transformContentSearcherSorts(_sort, commonContentTypeIds);
526            List<FacetDefinition> facets = _searchHelper.getFacetDefinitions(_facets, commonContentTypeIds);
527            
528            List<Query> filterQueries = new ArrayList<>();
529            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
530            
531            if (!_contentTypeIds.isEmpty())
532            {
533                filterQueries.add(_searchModelHelper.createContentTypeOrMixinQuery(_contentTypeIds));
534            }
535            
536            if (_filterQueries != null)
537            {
538                filterQueries.addAll(_filterQueries);
539            }
540            
541            List<String> filterQueryStrings = new ArrayList<>();
542            
543            if (_filterQueryStrings != null)
544            {
545                filterQueryStrings.addAll(_filterQueryStrings);
546            }
547            
548            Searcher searcher = _searcherFactory.create()
549                                   .withFilterQueries(filterQueries)
550                                   .withFilterQueryStrings(filterQueryStrings)
551                                   .withSort(sort)
552                                   .withFacets(facets)
553                                   .withFacetValues(facetValues)
554                                   .withLimits(_start, _maxResults)
555                                   .setCheckRights(_checkRights)
556                                   .withHighlightingQuery(_highlightingQuery);
557            
558            return searcher;
559        }
560    }
561    
562    /**
563     * Record representing a sort criterion.
564     * @param sortField The sort field. Can be a path to an element
565     * @param order The sort order
566     */
567    public record ContentSearchSort (String sortField, SortOrder order) { /* empty */ }
568}