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.Set;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032
033import org.ametys.cms.content.indexing.solr.SolrFieldNames;
034import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
035import org.ametys.cms.contenttype.ContentTypesHelper;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.QueryBuilder;
038import org.ametys.cms.search.SearchField;
039import org.ametys.cms.search.SearchResults;
040import org.ametys.cms.search.Sort;
041import org.ametys.cms.search.Sort.Order;
042import org.ametys.cms.search.filter.AccessSearchFilter;
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.ContentTypeQuery;
048import org.ametys.cms.search.query.DocumentTypeQuery;
049import org.ametys.cms.search.query.Query;
050import org.ametys.cms.search.solr.SearcherFactory;
051import org.ametys.cms.search.solr.SearcherFactory.Searcher;
052import org.ametys.cms.search.ui.model.SearchUIModel;
053import org.ametys.plugins.repository.AmetysObject;
054import org.ametys.plugins.repository.AmetysObjectIterable;
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 SearchUIModel _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            // TODO Do not cast.
145            this._searchModel = (SearchUIModel) searchModel;
146            this._sort = new ArrayList<>();
147            this._searchMode = "simple";
148            this._start = 0;
149            this._maxResults = Integer.MAX_VALUE;
150            this._checkRights = true;
151        }
152        
153        /**
154         * Add a sort criterion.
155         * @param fieldRef The field reference (name of a SearchField).
156         * @param order The sort order.
157         * @return The ContentSearcher itself.
158         */
159        public SearchModelContentSearcher addSort(String fieldRef, Order order)
160        {
161            _sort.add(new Sort(fieldRef, order));
162            return this;
163        }
164        
165        /**
166         * Set the sort criteria.
167         * @param sortCriteria The sort criteria as a List.
168         * @return The ContentSearcher itself.
169         */
170        public SearchModelContentSearcher withSort(List<Sort> sortCriteria)
171        {
172            _sort = new ArrayList<>(sortCriteria);
173            return this;
174        }
175        
176        /**
177         * Set the search mode.
178         * @param searchMode The search mode.
179         * @return The ContentSearcher itself.
180         */
181        public SearchModelContentSearcher withSearchMode(String searchMode)
182        {
183            _searchMode = searchMode;
184            return this;
185        }
186        
187        /**
188         * Set the limits to use.
189         * @param start The start index.
190         * @param maxResults The maximum number of results.
191         * @return The ContentSearcher itself.
192         */
193        public SearchModelContentSearcher withLimits(int start, int maxResults)
194        {
195            this._start = start;
196            this._maxResults = maxResults;
197            return this;
198        }
199        
200        /**
201         * Whether to check rights when searching, false otherwise.
202         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
203         * @return The ContentSearcher itself.
204         */
205        public SearchModelContentSearcher setCheckRights(boolean checkRights)
206        {
207            _checkRights = checkRights;
208            return this;
209        }
210        
211        /**
212         * Search the contents.
213         * @param values The values for search criteria defined in the model.
214         * @param <C> The type Content
215         * @return The search results as {@link AmetysObject}s.
216         * @throws Exception if an error occurs.
217         */
218        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception
219        {
220            return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search();
221        }
222        
223        /**
224         * Search the contents.
225         * @param values The values for search criteria defined in the model.
226         * @param <C> The type Content         * 
227         * @return The search results.
228         * @throws Exception if an error occurs.
229         */
230        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception
231        {
232            return searchWithFacets(values, Collections.emptyMap());
233        }
234        
235        /**
236         * Search the contents.
237         * @param <C> The type Content
238         * @param values The values for search criteria defined in the model.
239         * @param contextualParameters The search contextual parameters.
240         * @return The search results.
241         * @throws Exception if an error occurs.
242         */
243        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
244        {
245            return searchWithFacets(values, Collections.emptyMap(), contextualParameters);
246        }
247        
248        /**
249         * Search the contents.
250         * @param <C> The type Content
251         * @param values The values for search criteria defined in the model.
252         * @param facetValues The facet values, indexed 
253         * @param contextualParameters The search contextual parameters. 
254         * @return The search results.
255         * @throws Exception if an error occurs.
256         */
257        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception
258        {
259            return _searcher(values, facetValues, contextualParameters).searchWithFacets();
260        }
261        
262        private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters)
263        {
264            Query query = _queryBuilder.build(_searchModel, _searchMode, values, contextualParameters);
265            
266            List<Sort> sort = getSort(contextualParameters);
267            List<SearchField> facets = getFacets(contextualParameters);
268            
269            return _searcherFactory.create()
270                                    .withQuery(query)
271                                    .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
272                                    .withSort(sort)
273                                    .withFacets(facets)
274                                    .withFacetValues(facetValues)
275                                    .withLimits(_start, _maxResults)
276                                    .addContextElement(AccessSearchFilter.OBJECT_TYPE, AccessSearchFilter.TYPE_CONTENT)
277                                    .setCheckRights(_checkRights);
278        }
279        
280        /**
281         * Get the sort criteria.
282         * @param contextualParameters The search contextual parameters.
283         * @return The sort criteria.
284         */
285        protected List<Sort> getSort(Map<String, Object> contextualParameters)
286        {
287            List<Sort> sort = new ArrayList<>();
288            
289            if (!_sort.isEmpty())
290            {
291                // Index criterion and results by search field name.
292                Map<String, SearchCriterion> criteriaByName = new HashMap<>();
293                for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values())
294                {
295                    if (criterion.getSearchField() != null)
296                    {
297                        criteriaByName.put(criterion.getSearchField().getName(), criterion);
298                    }
299                }
300                Map<String, ResultField> resultsByName = new HashMap<>();
301                for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values())
302                {
303                    if (resultField.getSearchField() != null)
304                    {
305                        resultsByName.put(resultField.getSearchField().getName(), resultField);
306                    }
307                }
308                
309                for (Sort sortCriterion : _sort)
310                {
311                    String id = sortCriterion.getField();
312                    
313                    SearchField searchField = null;
314                    if (criteriaByName.containsKey(id))
315                    {
316                        searchField = criteriaByName.get(id).getSearchField();
317                    }
318                    else if (resultsByName.containsKey(id))
319                    {
320                        searchField = resultsByName.get(id).getSearchField();
321                    }
322                    
323                    if (searchField != null)
324                    {
325                        sort.add(new Sort(searchField, sortCriterion.getOrder()));
326                    }
327                    else
328                    {
329                        throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model.");
330                    }
331                }
332            }
333            else
334            {
335                // Get the default sort from the search model.
336            }
337            
338            return sort;
339        }
340        
341        /**
342         * Get the facet fields.
343         * @param contextualParameters The search contextual parameters.
344         * @return The facet fields as a List.
345         */
346        protected List<SearchField> getFacets(Map<String, Object> contextualParameters)
347        {
348            List<SearchField> facets = new ArrayList<>();
349            
350            for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values())
351            {
352                if (criterion.getSearchField() != null)
353                {
354                    facets.add(criterion.getSearchField());
355                }
356            }
357            
358            return facets;
359        }
360        
361    }
362    
363    /**
364     * A ContentSearcher on a list of content types.
365     */
366    public class SimpleContentSearcher
367    {
368        
369        private Set<String> _contentTypes;
370        private List<Sort> _sort;
371        private List<String> _facets;
372        private int _start;
373        private int _maxResults;
374        private boolean _checkRights;
375        
376        /**
377         * Build a content searcher on a list of content types.
378         * @param contentTypes A collection of content types to search on.
379         */
380        public SimpleContentSearcher(Collection<String> contentTypes)
381        {
382            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
383            this._sort = new ArrayList<>();
384            this._facets = new ArrayList<>();
385            this._start = 0;
386            this._maxResults = Integer.MAX_VALUE;
387            this._checkRights = true;
388        }
389        
390        /**
391         * Set the sort criteria.
392         * @param sortCriteria The sort criteria as a List.
393         * @return The ContentSearcher itself.
394         */
395        public SimpleContentSearcher withSort(List<Sort> sortCriteria)
396        {
397            _sort = new ArrayList<>(sortCriteria);
398            return this;
399        }
400        
401        /**
402         * Add a sort criterion.
403         * @param fieldRef The field reference (name of a SearchField).
404         * @param order The sort order.
405         * @return The ContentSearcher itself.
406         */
407        public SimpleContentSearcher addSort(String fieldRef, Order order)
408        {
409            _sort.add(new Sort(fieldRef, order));
410            return this;
411        }
412        
413        /**
414         * Set the facets.
415         * @param facets The facets list.
416         * @return The ContentSearcher itself.
417         */
418        public SimpleContentSearcher withFacets(List<String> facets)
419        {
420            _facets = new ArrayList<>(facets);
421            return this;
422        }
423        
424        /**
425         * Set the facets.
426         * @param facets The facets list.
427         * @return The ContentSearcher itself.
428         */
429        public SimpleContentSearcher withFacets(String... facets)
430        {
431            _facets = Arrays.asList(facets);
432            return this;
433        }
434        
435        /**
436         * Set the limits to use.
437         * @param start The start index.
438         * @param maxResults The maximum number of results.
439         * @return The ContentSearcher itself.
440         */
441        public SimpleContentSearcher withLimits(int start, int maxResults)
442        {
443            this._start = start;
444            this._maxResults = maxResults;
445            return this;
446        }
447        
448        /**
449         * Whether to check rights when searching, false otherwise.
450         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
451         * @return The ContentSearcher itself.
452         */
453        public SimpleContentSearcher setCheckRights(boolean checkRights)
454        {
455            _checkRights = checkRights;
456            return this;
457        }
458        
459        /**
460         * Search the contents.
461         * @param <C> The type Content
462         * @param query The query to execute.
463         * @return The search results as {@link AmetysObject}s.
464         * @throws Exception if an error occurs.
465         */
466        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
467        {
468            return _searcher(query, Collections.emptyMap()).search();
469        }
470        
471        /**
472         * Search the contents.
473         * @param <C> The type Content
474         * @param query The query to execute.
475         * @return The search results.
476         * @throws Exception if an error occurs.
477         */
478        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
479        {
480            return searchWithFacets(query, Collections.emptyMap());
481        }
482        
483        /**
484         * Search the contents.
485         * @param <C> The type Content
486         * @param query The query to execute.
487         * @param facetValues The facet values.
488         * @return The search results.
489         * @throws Exception if an error occurs.
490         */
491        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
492        {
493            return _searcher(query, facetValues).searchWithFacets();
494        }
495        
496        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
497        {
498            List<Sort> sort = getSort();
499            List<SearchField> facets = getFacets();
500            
501            List<Query> queries = new ArrayList<>();
502            queries.add(query);
503            
504            List<Query> filterQueries = new ArrayList<>();
505            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
506            
507            if (!_contentTypes.isEmpty())
508            {
509                filterQueries.add(new ContentTypeQuery(_contentTypes));
510            }
511            
512            return _searcherFactory.create()
513                                    .withQuery(query)
514                                    .withFilterQueries(filterQueries)
515                                    .withSort(sort)
516                                    .withFacets(facets)
517                                    .withFacetValues(facetValues)
518                                    .withLimits(_start, _maxResults)
519                                    .addContextElement(AccessSearchFilter.OBJECT_TYPE, AccessSearchFilter.TYPE_CONTENT)
520                                    .setCheckRights(_checkRights);
521        }
522        
523        /**
524         * Get the sort criteria from the specified field names.
525         * @return The sort criteria.
526         */
527        protected List<Sort> getSort()
528        {
529            List<Sort> sortCriteria = new ArrayList<>();
530            
531            for (Sort sort : _sort)
532            {
533                String fieldName = sort.getField();
534                Order order = sort.getOrder();
535                
536                SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
537//                SearchField searchField = getSearchField(_contentTypes, fieldName);
538                if (searchField != null)
539                {
540                    sortCriteria.add(new Sort(searchField, order));
541                }
542            }
543            
544            return sortCriteria;
545        }
546        
547        /**
548         * Get the facet criteria as a list of SearchField from the specified field names.
549         * @return The facets as a List of SearchField. 
550         */
551        protected List<SearchField> getFacets()
552        {
553            List<SearchField> facets = new ArrayList<>();
554            
555            for (String fieldName : _facets)
556            {
557                SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
558//                SearchField searchField = getSearchField(_contentTypes, fieldName);
559                if (searchField != null)
560                {
561                    facets.add(searchField);
562                }
563            }
564            
565            return facets;
566        }
567        
568//        /**
569//         * Get a {@link SearchField} from a field name.
570//         * @param fieldName The field name, can be either a system property ID or a metadata path (not joined).
571//         * @return The {@link SearchField} corresponding to the 
572//         */
573//        public SearchField getSearchField(Collection<String> contentTypes, String fieldName)
574//        {
575//            SearchField searchField = null;
576//            
577//            if (_sysPropEP.hasExtension(fieldName))
578//            {
579//                SystemProperty property = _sysPropEP.getExtension(fieldName);
580//                searchField = property.getSearchField();
581//            }
582//            else
583//            {
584//                String metadataPath = fieldName.replace('.', '/');
585//                _searchHelper.getMetadataSearchField(contentTypes, metadataPath);
586//            }
587//            
588//            return searchField;
589//        }
590        
591//        /**
592//         * Get a {@link SearchField} from a field name.
593//         * @param fieldName The field name, can be either a system property ID or a metadata path (not joined).
594//         * @return The {@link SearchField} corresponding to the 
595//         */
596//        protected SearchField getSearchField(String fieldName)
597//        {
598//            SearchField searchField = null;
599//            
600//            if (_sysPropEP.hasExtension(fieldName))
601//            {
602//                SystemProperty property = _sysPropEP.getExtension(fieldName);
603//                searchField = property.getSearchField();
604//            }
605//            else
606//            {
607//                String metaPath = fieldName.replace('.', '/');
608//                if (_contentTypeId != null)
609//                {
610//                    ContentType cType = _cTypeEP.getExtension(_contentTypeId);
611//                    List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionsByPath(cType, metaPath);
612//                    
613//                    boolean joinedMetadata = isJoinedMetadata(metaDefs);
614//                    
615//                    if (!joinedMetadata)
616//                    {
617//                        MetadataType type = metaDefs.get(metaDefs.size() - 1).getType();
618//                        searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, type);
619//                    }
620//                    else
621//                    {
622//                        throw new IllegalArgumentException("The metadata '" + fieldName + "' can't be used as it is joined.");
623//                    }
624//                }
625//                else if (fieldName.equals("title"))
626//                {
627//                    // No specific content type: allow only title.
628//                    // TODO Provide a standard "title" metadata definition to avoid getting it on a random content type.
629//                    ContentType cType = _cTypeEP.getExtension(_contentTypes.iterator().next());
630//                    MetadataDefinition metaDef = cType.getMetadataDefinition("title");
631//                    searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, metaDef.getType());
632//                }
633//            }
634//            
635//            if (searchField == null)
636//            {
637//                throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the selected content types.");
638//            }
639//            
640//            return searchField;
641//        }
642//        
643//        /**
644//         * Test if a metadata, represented by a list of successive definitions, is joined.
645//         * @param metaDefs The list of successive definitions.
646//         * @return <code>true</code> if the metadata is joined, <code>false</code> otherwise.
647//         */
648//        protected boolean isJoinedMetadata(List<MetadataDefinition> metaDefs)
649//        {
650//            boolean joinedMetadata = false;
651//            Iterator<MetadataDefinition> metaDefIt = metaDefs.iterator();
652//            while (metaDefIt.hasNext())
653//            {
654//                MetadataType type = metaDefIt.next().getType();
655//                // The column represents a "joined" value if it has a content metadata (except if it's the last one).
656//                if ((type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) && metaDefIt.hasNext())
657//                {
658//                    joinedMetadata = true;
659//                }
660//            }
661//            return joinedMetadata;
662//        }
663        
664    }
665    
666}