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.ResultField;
044import org.ametys.cms.search.model.SearchCriterion;
045import org.ametys.cms.search.model.SearchModel;
046import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
047import org.ametys.cms.search.query.DocumentTypeQuery;
048import org.ametys.cms.search.query.Query;
049import org.ametys.cms.search.solr.SearcherFactory;
050import org.ametys.cms.search.solr.SearcherFactory.Searcher;
051import org.ametys.cms.search.ui.model.SearchUIModel;
052import org.ametys.plugins.repository.AmetysObject;
053import org.ametys.plugins.repository.AmetysObjectIterable;
054import org.ametys.runtime.plugin.component.AbstractLogEnabled;
055
056/**
057 * Component creating content searchers from {@link SearchModel}s or content type IDs.
058 */
059public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable
060{
061    
062    /** The component role. */
063    public static final String ROLE = ContentSearcherFactory.class.getName();
064    
065    /** The searcher factory. */
066    protected SearcherFactory _searcherFactory;
067    
068    /** The query builder. */
069    protected QueryBuilder _queryBuilder;
070    
071    /** The content type extension point. */
072    protected ContentTypeExtensionPoint _cTypeEP;
073    
074    /** The content type helper. */
075    protected ContentTypesHelper _cTypeHelper;
076    
077    /** The system property extension point. */
078    protected SystemPropertyExtensionPoint _sysPropEP;
079    
080    /** The search helper. */
081    protected ContentSearchHelper _searchHelper;
082    
083    @Override
084    public void service(ServiceManager manager) throws ServiceException
085    {
086        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
087        _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE);
088        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
089        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
090        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
091        
092        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
093    }
094    
095    /**
096     * Create a ContentSearcher from a search model.
097     * @param searchModel The reference search model.
098     * @return a ContentSearcher backed by the given search model.
099     */
100    public SearchModelContentSearcher create(SearchModel searchModel)
101    {
102        return new SearchModelContentSearcher(searchModel);
103    }
104    
105    /**
106     * Create a simple ContentSearcher from a list of content types.
107     * @param contentTypes The content types to search on.
108     * @return a ContentSearcher referencing the given content types.
109     */
110    public SimpleContentSearcher create(String... contentTypes)
111    {
112        return new SimpleContentSearcher(Arrays.asList(contentTypes));
113    }
114    
115    /**
116     * Create a simple ContentSearcher from a list of content types.
117     * @param contentTypes The content types to search on.
118     * @return a ContentSearcher referencing the given content types.
119     */
120    public SimpleContentSearcher create(Collection<String> contentTypes)
121    {
122        return new SimpleContentSearcher(contentTypes);
123    }
124    
125    /**
126     * A ContentSearcher backed by a {@link SearchModel}.
127     */
128    public class SearchModelContentSearcher
129    {
130        private SearchUIModel _searchModel;
131        private List<Sort> _sort;
132        private String _searchMode;
133        private int _start;
134        private int _maxResults;
135        private boolean _checkRights;
136        
137        /**
138         * Build a ContentSearcher referencing a {@link SearchModel}.
139         * @param searchModel the {@link SearchModel}.
140         */
141        public SearchModelContentSearcher(SearchModel searchModel)
142        {
143            // TODO Do not cast.
144            this._searchModel = (SearchUIModel) 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                Map<String, ResultField> resultsByName = new HashMap<>();
313                for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values())
314                {
315                    if (resultField.getSearchField() != null)
316                    {
317                        resultsByName.put(resultField.getSearchField().getName(), resultField);
318                    }
319                }
320                
321                for (Sort sortCriterion : _sort)
322                {
323                    String id = sortCriterion.getField();
324                    
325                    SearchField searchField = null;
326                    if (criteriaByName.containsKey(id))
327                    {
328                        searchField = criteriaByName.get(id).getSearchField();
329                    }
330                    else if (resultsByName.containsKey(id))
331                    {
332                        searchField = resultsByName.get(id).getSearchField();
333                    }
334                    
335                    if (searchField == null)
336                    {
337                        throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model.");
338                    }
339                    else if (searchField.getSortField() == null)
340                    {
341                        getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id);
342                    }
343                    else
344                    {
345                        sort.add(new Sort(searchField, sortCriterion.getOrder()));
346                    }
347                }
348            }
349            else
350            {
351                // Get the default sort from the search model.
352            }
353            
354            return sort;
355        }
356        
357        /**
358         * Get the facet fields.
359         * @param contextualParameters The search contextual parameters.
360         * @return The facet fields as a List.
361         */
362        protected List<SearchField> getFacets(Map<String, Object> contextualParameters)
363        {
364            List<SearchField> facets = new ArrayList<>();
365            
366            for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values())
367            {
368                if (criterion.getSearchField() != null)
369                {
370                    facets.add(criterion.getSearchField());
371                }
372            }
373            
374            return facets;
375        }
376        
377    }
378    
379    /**
380     * A ContentSearcher on a list of content types.
381     */
382    public class SimpleContentSearcher
383    {
384        
385        private Set<String> _contentTypes;
386        private List<Sort> _sort;
387        private List<String> _facets;
388        private int _start;
389        private int _maxResults;
390        private boolean _checkRights;
391        private List<String> _filterQueryStrings;
392        private List<Query> _filterQueries;
393        
394        /**
395         * Build a content searcher on a list of content types.
396         * @param contentTypes A collection of content types to search on.
397         */
398        public SimpleContentSearcher(Collection<String> contentTypes)
399        {
400            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
401            this._sort = new ArrayList<>();
402            this._facets = new ArrayList<>();
403            this._start = 0;
404            this._maxResults = Integer.MAX_VALUE;
405            this._checkRights = true;
406        }
407        
408        /**
409         * Set the filter queries.
410         * @param filterQueries the filter queries.
411         * @return The ContentSearcher itself.
412         */
413        public SimpleContentSearcher withFilterQueries(List<Query> filterQueries)
414        {
415            _filterQueries = filterQueries;
416            return this;
417        }
418        
419        /**
420         * Set the filter queries.
421         * @param filterQueryStrings the filter queries.
422         * @return The ContentSearcher itself.
423         */
424        public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings)
425        {
426            _filterQueryStrings = filterQueryStrings;
427            return this;
428        }
429        
430        /**
431         * Set the sort criteria.
432         * @param sortCriteria The sort criteria as a List.
433         * @return The ContentSearcher itself.
434         */
435        public SimpleContentSearcher withSort(List<Sort> sortCriteria)
436        {
437            _sort = new ArrayList<>(sortCriteria);
438            return this;
439        }
440        
441        /**
442         * Add a sort criterion.
443         * @param fieldRef The field reference (name of a SearchField).
444         * @param order The sort order.
445         * @return The ContentSearcher itself.
446         */
447        public SimpleContentSearcher addSort(String fieldRef, Order order)
448        {
449            _sort.add(new Sort(fieldRef, order));
450            return this;
451        }
452        
453        /**
454         * Set the facets.
455         * @param facets The facets list.
456         * @return The ContentSearcher itself.
457         */
458        public SimpleContentSearcher withFacets(Collection<String> facets)
459        {
460            _facets = new ArrayList<>(facets);
461            return this;
462        }
463        
464        /**
465         * Set the facets.
466         * @param facets The facets list.
467         * @return The ContentSearcher itself.
468         */
469        public SimpleContentSearcher withFacets(String... facets)
470        {
471            _facets = Arrays.asList(facets);
472            return this;
473        }
474        
475        /**
476         * Set the limits to use.
477         * @param start The start index.
478         * @param maxResults The maximum number of results.
479         * @return The ContentSearcher itself.
480         */
481        public SimpleContentSearcher withLimits(int start, int maxResults)
482        {
483            this._start = start;
484            this._maxResults = maxResults;
485            return this;
486        }
487        
488        /**
489         * Whether to check rights when searching, false otherwise.
490         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
491         * @return The ContentSearcher itself.
492         */
493        public SimpleContentSearcher setCheckRights(boolean checkRights)
494        {
495            _checkRights = checkRights;
496            return this;
497        }
498        
499        /**
500         * Search the contents.
501         * @param <C> The type Content
502         * @param query The query object to execute.
503         * @return The search results as {@link AmetysObject}s.
504         * @throws Exception if an error occurs.
505         */
506        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
507        {
508            return _searcher(query, Collections.emptyMap()).search();
509        }
510        
511        /**
512         * Search the contents.
513         * @param <C> The type Content
514         * @param query The query string to execute.
515         * @return The search results as {@link AmetysObject}s.
516         * @throws Exception if an error occurs.
517         */
518        public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception
519        {
520            return _searcher(query, Collections.emptyMap()).search();
521        }
522        
523        /**
524         * Search the contents.
525         * @param <C> The type Content
526         * @param query The query objet to execute.
527         * @return The search results.
528         * @throws Exception if an error occurs.
529         */
530        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
531        {
532            return searchWithFacets(query, Collections.emptyMap());
533        }
534        
535        /**
536         * Search the contents.
537         * @param <C> The type Content
538         * @param query The query string to execute.
539         * @return The search results.
540         * @throws Exception if an error occurs.
541         */
542        public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception
543        {
544            return searchWithFacets(query, Collections.emptyMap());
545        }
546        
547        /**
548         * Search the contents.
549         * @param <C> The type Content
550         * @param query The query object to execute.
551         * @param facetValues The facet values.
552         * @return The search results.
553         * @throws Exception if an error occurs.
554         */
555        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
556        {
557            return _searcher(query, facetValues).searchWithFacets();
558        }
559        
560        /**
561         * Search the contents.
562         * @param <C> The type Content
563         * @param query The query string to execute.
564         * @param facetValues The facet values.
565         * @return The search results.
566         * @throws Exception if an error occurs.
567         */
568        public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception
569        {
570            return _searcher(query, facetValues).searchWithFacets();
571        }
572        
573        private Searcher _searcher(String query, Map<String, List<String>> facetValues)
574        {
575            return _searcher(facetValues).withQueryString(query);
576        }
577        
578        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
579        {
580            return _searcher(facetValues).withQuery(query);
581        }
582        
583        private Searcher _searcher(Map<String, List<String>> facetValues)
584        {
585            List<Sort> sort = getSort();
586            List<SearchField> facets = getFacets();
587            
588            List<Query> filterQueries = new ArrayList<>();
589            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
590            
591            if (!_contentTypes.isEmpty())
592            {
593                filterQueries.add(_queryBuilder.createContentTypeOrMixinQuery(_contentTypes, null, true));
594            }
595            
596            if (_filterQueries != null)
597            {
598                filterQueries.addAll(_filterQueries);
599            }
600            
601            List<String> filterQueryStrings = new ArrayList<>();
602            
603            if (_filterQueryStrings != null)
604            {
605                filterQueryStrings.addAll(_filterQueryStrings);
606            }
607            
608            return _searcherFactory.create()
609                                   .withFilterQueries(filterQueries)
610                                   .withFilterQueryStrings(filterQueryStrings)
611                                   .withSort(sort)
612                                   .withFacets(facets)
613                                   .withFacetValues(facetValues)
614                                   .withLimits(_start, _maxResults)
615                                   .setCheckRights(_checkRights);
616        }
617        
618        /**
619         * Get the sort criteria from the specified field names.
620         * @return The sort criteria.
621         */
622        protected List<Sort> getSort()
623        {
624            List<Sort> sortCriteria = new ArrayList<>();
625            
626            for (Sort sort : _sort)
627            {
628                String fieldName = sort.getField();
629                Order order = sort.getOrder();
630                
631                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
632//                SearchField searchField = getSearchField(_contentTypes, fieldName);
633                if (searchField.isPresent())
634                {
635                    sortCriteria.add(new Sort(searchField.get(), order));
636                }
637                else
638                {
639                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
640                }
641            }
642            
643            return sortCriteria;
644        }
645        
646        /**
647         * Get the facet criteria as a list of SearchField from the specified field names.
648         * @return The facets as a List of SearchField. 
649         */
650        protected List<SearchField> getFacets()
651        {
652            List<SearchField> facets = new ArrayList<>();
653            
654            for (String fieldName : _facets)
655            {
656                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
657                if (searchField.isPresent())
658                {
659                    facets.add(searchField.get());
660                }
661                else
662                {
663                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
664                }
665            }
666            
667            return facets;
668        }
669        
670        private String _exceptionMessageForEmptySearchField(String fieldName)
671        {
672            return "The field '" + fieldName + "' can't be found in the selected content types.";
673        }
674    }
675}