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.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033
034import org.ametys.cms.content.indexing.solr.SolrFieldNames;
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.contenttype.ContentTypesHelper;
037import org.ametys.cms.repository.Content;
038import org.ametys.cms.search.QueryBuilder;
039import org.ametys.cms.search.SearchField;
040import org.ametys.cms.search.SearchResults;
041import org.ametys.cms.search.Sort;
042import org.ametys.cms.search.Sort.Order;
043import org.ametys.cms.search.model.SearchCriterion;
044import org.ametys.cms.search.model.SearchModel;
045import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
046import org.ametys.cms.search.query.DocumentTypeQuery;
047import org.ametys.cms.search.query.Query;
048import org.ametys.cms.search.solr.SearcherFactory;
049import org.ametys.cms.search.solr.SearcherFactory.Searcher;
050import org.ametys.plugins.repository.AmetysObject;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.runtime.model.ModelViewItem;
053import org.ametys.runtime.model.ViewItem;
054import org.ametys.runtime.model.ViewItemAccessor;
055import org.ametys.runtime.plugin.component.AbstractLogEnabled;
056
057/**
058 * Component creating content searchers from {@link SearchModel}s or content type IDs.
059 */
060public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable
061{
062    
063    /** The component role. */
064    public static final String ROLE = ContentSearcherFactory.class.getName();
065    
066    /** The searcher factory. */
067    protected SearcherFactory _searcherFactory;
068    
069    /** The query builder. */
070    protected QueryBuilder _queryBuilder;
071    
072    /** The content type extension point. */
073    protected ContentTypeExtensionPoint _cTypeEP;
074    
075    /** The content type helper. */
076    protected ContentTypesHelper _cTypeHelper;
077    
078    /** The system property extension point. */
079    protected SystemPropertyExtensionPoint _sysPropEP;
080    
081    /** The search helper. */
082    protected ContentSearchHelper _searchHelper;
083    
084    @Override
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
088        _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE);
089        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
090        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
091        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
092        
093        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
094    }
095    
096    /**
097     * Create a ContentSearcher from a search model.
098     * @param searchModel The reference search model.
099     * @return a ContentSearcher backed by the given search model.
100     */
101    public SearchModelContentSearcher create(SearchModel searchModel)
102    {
103        return new SearchModelContentSearcher(searchModel);
104    }
105    
106    /**
107     * Create a simple ContentSearcher from a list of content types.
108     * @param contentTypes The content types to search on.
109     * @return a ContentSearcher referencing the given content types.
110     */
111    public SimpleContentSearcher create(String... contentTypes)
112    {
113        return new SimpleContentSearcher(Arrays.asList(contentTypes));
114    }
115    
116    /**
117     * Create a simple ContentSearcher from a list of content types.
118     * @param contentTypes The content types to search on.
119     * @return a ContentSearcher referencing the given content types.
120     */
121    public SimpleContentSearcher create(Collection<String> contentTypes)
122    {
123        return new SimpleContentSearcher(contentTypes);
124    }
125    
126    /**
127     * A ContentSearcher backed by a {@link SearchModel}.
128     */
129    public class SearchModelContentSearcher
130    {
131        private SearchModel _searchModel;
132        private List<Sort> _sort;
133        private String _searchMode;
134        private int _start;
135        private int _maxResults;
136        private boolean _checkRights;
137        
138        /**
139         * Build a ContentSearcher referencing a {@link SearchModel}.
140         * @param searchModel the {@link SearchModel}.
141         */
142        public SearchModelContentSearcher(SearchModel searchModel)
143        {
144            this._searchModel = searchModel;
145            this._sort = new ArrayList<>();
146            this._searchMode = "simple";
147            this._start = 0;
148            this._maxResults = Integer.MAX_VALUE;
149            this._checkRights = true;
150        }
151        
152        /**
153         * Add a sort criterion.
154         * @param fieldRef The field reference (name of a SearchField).
155         * @param order The sort order.
156         * @return The ContentSearcher itself.
157         */
158        public SearchModelContentSearcher addSort(String fieldRef, Order order)
159        {
160            _sort.add(new Sort(fieldRef, order));
161            return this;
162        }
163        
164        /**
165         * Set the sort criteria.
166         * @param sortCriteria The sort criteria as a List.
167         * @return The ContentSearcher itself.
168         */
169        public SearchModelContentSearcher withSort(List<Sort> sortCriteria)
170        {
171            _sort = new ArrayList<>(sortCriteria);
172            return this;
173        }
174        
175        /**
176         * Set the search mode.
177         * @param searchMode The search mode.
178         * @return The ContentSearcher itself.
179         */
180        public SearchModelContentSearcher withSearchMode(String searchMode)
181        {
182            _searchMode = searchMode;
183            return this;
184        }
185        
186        /**
187         * Set the limits to use.
188         * @param start The start index.
189         * @param maxResults The maximum number of results.
190         * @return The ContentSearcher itself.
191         */
192        public SearchModelContentSearcher withLimits(int start, int maxResults)
193        {
194            this._start = start;
195            this._maxResults = maxResults;
196            return this;
197        }
198        
199        /**
200         * Whether to check rights when searching, false otherwise.
201         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
202         * @return The ContentSearcher itself.
203         */
204        public SearchModelContentSearcher setCheckRights(boolean checkRights)
205        {
206            _checkRights = checkRights;
207            return this;
208        }
209        
210        /**
211         * Search the contents.
212         * @param values The values for search criteria defined in the model.
213         * @param <C> The type Content
214         * @return The search results as {@link AmetysObject}s.
215         * @throws Exception if an error occurs.
216         */
217        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception
218        {
219            return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search();
220        }
221        
222        /**
223         * Search the contents.
224         * @param values The values for search criteria defined in the model.
225         * @param contextualParameters The search contextual parameters.
226         * @param <C> The type Content
227         * @return The search results as {@link AmetysObject}s.
228         * @throws Exception if an error occurs.
229         */
230        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
231        {
232            return _searcher(values, Collections.emptyMap(), contextualParameters).search();
233        }
234        
235        /**
236         * Search the contents.
237         * @param values The values for search criteria defined in the model.
238         * @param <C> The type Content         * 
239         * @return The search results.
240         * @throws Exception if an error occurs.
241         */
242        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception
243        {
244            return searchWithFacets(values, Collections.emptyMap());
245        }
246        
247        /**
248         * Search the contents.
249         * @param <C> The type Content
250         * @param values The values for search criteria defined in the model.
251         * @param contextualParameters The search contextual parameters.
252         * @return The search results.
253         * @throws Exception if an error occurs.
254         */
255        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
256        {
257            return searchWithFacets(values, Collections.emptyMap(), contextualParameters);
258        }
259        
260        /**
261         * Search the contents.
262         * @param <C> The type Content
263         * @param values The values for search criteria defined in the model.
264         * @param facetValues The facet values, indexed 
265         * @param contextualParameters The search contextual parameters. 
266         * @return The search results.
267         * @throws Exception if an error occurs.
268         */
269        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception
270        {
271            return _searcher(values, facetValues, contextualParameters).searchWithFacets();
272        }
273        
274        private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters)
275        {
276            Query query = _queryBuilder.build(_searchModel, _searchMode, true, values, contextualParameters);
277            
278            List<Sort> sort = getSort(contextualParameters);
279            List<SearchField> facets = getFacets(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         * Get the sort criteria.
293         * @param contextualParameters The search contextual parameters.
294         * @return The sort criteria.
295         */
296        @SuppressWarnings("synthetic-access")
297        protected List<Sort> getSort(Map<String, Object> contextualParameters)
298        {
299            List<Sort> sort = new ArrayList<>();
300            
301            if (!_sort.isEmpty())
302            {
303                // Index criterion and results by search field name.
304                Map<String, SearchCriterion> criteriaByName = new HashMap<>();
305                for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values())
306                {
307                    if (criterion.getSearchField() != null)
308                    {
309                        criteriaByName.put(criterion.getSearchField().getName(), criterion);
310                    }
311                }
312                
313                Map<String, SearchField> resultSearchFields = _getResultSearchFields(_searchModel.getResultItems(contextualParameters));
314                
315                for (Sort sortCriterion : _sort)
316                {
317                    String id = sortCriterion.getField();
318                    
319                    SearchField searchField = null;
320                    if (criteriaByName.containsKey(id))
321                    {
322                        searchField = criteriaByName.get(id).getSearchField();
323                    }
324                    else if (resultSearchFields.containsKey(id))
325                    {
326                        searchField = resultSearchFields.get(id);
327                    }
328                    
329                    if (searchField == null)
330                    {
331                        throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model.");
332                    }
333                    else if (searchField.getSortField() == null)
334                    {
335                        getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id);
336                    }
337                    else
338                    {
339                        sort.add(new Sort(searchField, sortCriterion.getOrder()));
340                    }
341                }
342            }
343            else
344            {
345                // Get the default sort from the search model.
346            }
347            
348            return sort;
349        }
350        
351        private Map<String, SearchField> _getResultSearchFields(ViewItemAccessor viewItemAccessor)
352        {
353            Map<String, SearchField> searchFields = new HashMap<>();
354            for (ViewItem viewItem : viewItemAccessor.getViewItems())
355            {
356                if (viewItem instanceof ViewItemAccessor itemAccessor && !itemAccessor.getViewItems().isEmpty())
357                {
358                    searchFields.putAll(_getResultSearchFields(itemAccessor));
359                }
360                else if (viewItem instanceof ModelViewItem modelViewItem)
361                {
362                    // this item is a leaf, add its search field to the results map
363                    SearchField searchField = _searchHelper.getSearchField(modelViewItem);
364                    if (searchField != null)
365                    {
366                        searchFields.put(searchField.getName(), searchField);
367                    }
368                }
369            }
370            
371            return searchFields;
372        }
373        
374        /**
375         * Get the facet fields.
376         * @param contextualParameters The search contextual parameters.
377         * @return The facet fields as a List.
378         */
379        protected List<SearchField> getFacets(Map<String, Object> contextualParameters)
380        {
381            List<SearchField> facets = new ArrayList<>();
382            
383            for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values())
384            {
385                if (criterion.getSearchField() != null)
386                {
387                    facets.add(criterion.getSearchField());
388                }
389            }
390            
391            return facets;
392        }
393        
394    }
395    
396    /**
397     * A ContentSearcher on a list of content types.
398     */
399    public class SimpleContentSearcher
400    {
401        
402        private Set<String> _contentTypes;
403        private List<Sort> _sort;
404        private List<String> _facets;
405        private int _start;
406        private int _maxResults;
407        private boolean _checkRights;
408        private List<String> _filterQueryStrings;
409        private List<Query> _filterQueries;
410        
411        /**
412         * Build a content searcher on a list of content types.
413         * @param contentTypes A collection of content types to search on.
414         */
415        public SimpleContentSearcher(Collection<String> contentTypes)
416        {
417            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
418            this._sort = new ArrayList<>();
419            this._facets = new ArrayList<>();
420            this._start = 0;
421            this._maxResults = Integer.MAX_VALUE;
422            this._checkRights = true;
423        }
424        
425        /**
426         * Set the filter queries.
427         * @param filterQueries the filter queries.
428         * @return The ContentSearcher itself.
429         */
430        public SimpleContentSearcher withFilterQueries(List<Query> filterQueries)
431        {
432            _filterQueries = filterQueries;
433            return this;
434        }
435        
436        /**
437         * Set the filter queries.
438         * @param filterQueryStrings the filter queries.
439         * @return The ContentSearcher itself.
440         */
441        public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings)
442        {
443            _filterQueryStrings = filterQueryStrings;
444            return this;
445        }
446        
447        /**
448         * Set the sort criteria.
449         * @param sortCriteria The sort criteria as a List.
450         * @return The ContentSearcher itself.
451         */
452        public SimpleContentSearcher withSort(List<Sort> sortCriteria)
453        {
454            _sort = new ArrayList<>(sortCriteria);
455            return this;
456        }
457        
458        /**
459         * Add a sort criterion.
460         * @param fieldRef The field reference (name of a SearchField).
461         * @param order The sort order.
462         * @return The ContentSearcher itself.
463         */
464        public SimpleContentSearcher addSort(String fieldRef, Order order)
465        {
466            _sort.add(new Sort(fieldRef, order));
467            return this;
468        }
469        
470        /**
471         * Set the facets.
472         * @param facets The facets list.
473         * @return The ContentSearcher itself.
474         */
475        public SimpleContentSearcher withFacets(Collection<String> facets)
476        {
477            _facets = new ArrayList<>(facets);
478            return this;
479        }
480        
481        /**
482         * Set the facets.
483         * @param facets The facets list.
484         * @return The ContentSearcher itself.
485         */
486        public SimpleContentSearcher withFacets(String... facets)
487        {
488            _facets = Arrays.asList(facets);
489            return this;
490        }
491        
492        /**
493         * Set the limits to use.
494         * @param start The start index.
495         * @param maxResults The maximum number of results.
496         * @return The ContentSearcher itself.
497         */
498        public SimpleContentSearcher withLimits(int start, int maxResults)
499        {
500            this._start = start;
501            this._maxResults = maxResults;
502            return this;
503        }
504        
505        /**
506         * Whether to check rights when searching, false otherwise.
507         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
508         * @return The ContentSearcher itself.
509         */
510        public SimpleContentSearcher setCheckRights(boolean checkRights)
511        {
512            _checkRights = checkRights;
513            return this;
514        }
515        
516        /**
517         * Search the contents.
518         * @param <C> The type Content
519         * @param query The query object to execute.
520         * @return The search results as {@link AmetysObject}s.
521         * @throws Exception if an error occurs.
522         */
523        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
524        {
525            return _searcher(query, Collections.emptyMap()).search();
526        }
527        
528        /**
529         * Search the contents.
530         * @param <C> The type Content
531         * @param query The query string to execute.
532         * @return The search results as {@link AmetysObject}s.
533         * @throws Exception if an error occurs.
534         */
535        public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception
536        {
537            return _searcher(query, Collections.emptyMap()).search();
538        }
539        
540        /**
541         * Search the contents.
542         * @param <C> The type Content
543         * @param query The query objet to execute.
544         * @return The search results.
545         * @throws Exception if an error occurs.
546         */
547        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
548        {
549            return searchWithFacets(query, Collections.emptyMap());
550        }
551        
552        /**
553         * Search the contents.
554         * @param <C> The type Content
555         * @param query The query string to execute.
556         * @return The search results.
557         * @throws Exception if an error occurs.
558         */
559        public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception
560        {
561            return searchWithFacets(query, Collections.emptyMap());
562        }
563        
564        /**
565         * Search the contents.
566         * @param <C> The type Content
567         * @param query The query object to execute.
568         * @param facetValues The facet values.
569         * @return The search results.
570         * @throws Exception if an error occurs.
571         */
572        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
573        {
574            return _searcher(query, facetValues).searchWithFacets();
575        }
576        
577        /**
578         * Search the contents.
579         * @param <C> The type Content
580         * @param query The query string to execute.
581         * @param facetValues The facet values.
582         * @return The search results.
583         * @throws Exception if an error occurs.
584         */
585        public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception
586        {
587            return _searcher(query, facetValues).searchWithFacets();
588        }
589        
590        private Searcher _searcher(String query, Map<String, List<String>> facetValues)
591        {
592            return _searcher(facetValues).withQueryString(query);
593        }
594        
595        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
596        {
597            return _searcher(facetValues).withQuery(query);
598        }
599        
600        private Searcher _searcher(Map<String, List<String>> facetValues)
601        {
602            List<Sort> sort = getSort();
603            List<SearchField> facets = getFacets();
604            
605            List<Query> filterQueries = new ArrayList<>();
606            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
607            
608            if (!_contentTypes.isEmpty())
609            {
610                filterQueries.add(_queryBuilder.createContentTypeOrMixinQuery(_contentTypes, null, true));
611            }
612            
613            if (_filterQueries != null)
614            {
615                filterQueries.addAll(_filterQueries);
616            }
617            
618            List<String> filterQueryStrings = new ArrayList<>();
619            
620            if (_filterQueryStrings != null)
621            {
622                filterQueryStrings.addAll(_filterQueryStrings);
623            }
624            
625            return _searcherFactory.create()
626                                   .withFilterQueries(filterQueries)
627                                   .withFilterQueryStrings(filterQueryStrings)
628                                   .withSort(sort)
629                                   .withFacets(facets)
630                                   .withFacetValues(facetValues)
631                                   .withLimits(_start, _maxResults)
632                                   .setCheckRights(_checkRights);
633        }
634        
635        /**
636         * Get the sort criteria from the specified field names.
637         * @return The sort criteria.
638         */
639        protected List<Sort> getSort()
640        {
641            List<Sort> sortCriteria = new ArrayList<>();
642            
643            for (Sort sort : _sort)
644            {
645                String fieldName = sort.getField();
646                Order order = sort.getOrder();
647                
648                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
649//                SearchField searchField = getSearchField(_contentTypes, fieldName);
650                if (searchField.isPresent())
651                {
652                    sortCriteria.add(new Sort(searchField.get(), order));
653                }
654                else
655                {
656                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
657                }
658            }
659            
660            return sortCriteria;
661        }
662        
663        /**
664         * Get the facet criteria as a list of SearchField from the specified field names.
665         * @return The facets as a List of SearchField. 
666         */
667        protected List<SearchField> getFacets()
668        {
669            List<SearchField> facets = new ArrayList<>();
670            
671            for (String fieldName : _facets)
672            {
673                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
674                if (searchField.isPresent())
675                {
676                    facets.add(searchField.get());
677                }
678                else
679                {
680                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
681                }
682            }
683            
684            return facets;
685        }
686        
687        private String _exceptionMessageForEmptySearchField(String fieldName)
688        {
689            return "The field '" + fieldName + "' can't be found in the selected content types.";
690        }
691    }
692}