001/*
002 *  Copyright 2013 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.ui.model;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.activity.Disposable;
030import org.apache.avalon.framework.component.ComponentException;
031import org.apache.avalon.framework.configuration.Configurable;
032import org.apache.avalon.framework.configuration.Configuration;
033import org.apache.avalon.framework.configuration.ConfigurationException;
034import org.apache.avalon.framework.context.Context;
035import org.apache.avalon.framework.context.ContextException;
036import org.apache.avalon.framework.context.Contextualizable;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.cocoon.components.LifecycleHelper;
041import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
042import org.apache.commons.lang3.StringUtils;
043
044import org.ametys.cms.contenttype.ContentType;
045import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
046import org.ametys.cms.contenttype.ContentTypesHelper;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.search.model.SearchCriterionHelper;
049import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
050import org.ametys.cms.search.query.Query.Operator;
051import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
052import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.runtime.model.ElementDefinition;
055import org.ametys.runtime.model.ModelHelper.ConfigurationAndPluginName;
056import org.ametys.runtime.model.ModelItem;
057import org.ametys.runtime.model.ViewParser;
058import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
059
060/**
061 * Static implementation of a {@link AbstractSearchUIModel}
062 */
063public class StaticSearchUIModel extends AbstractSearchUIModel implements Serviceable, Contextualizable, Configurable, Disposable
064{
065    /** ComponentManager for {@link SearchUICriterion}s. */
066    protected ThreadSafeComponentManager<SearchUICriterion> _searchCriterionManager;
067    
068    /** The context. */
069    protected Context _context;
070    
071    /** The service manager */
072    protected ServiceManager _manager;
073    
074    /** The content type helper. */
075    protected ContentTypesHelper _contentTypesHelper;
076    
077    /** The helper for columns */
078    protected ColumnHelper _columnHelper;
079    
080    /** The content type extension point */
081    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
082    
083    /** The system property extension point. */
084    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
085    
086    /** The search criterion helper */
087    protected SearchCriterionHelper _searchCriterionHelper;
088    
089    private int _criteriaIndex;
090    
091    private int _pageSize;
092    private String _workspace;
093    private String _searchUrl;
094    private String _searchUrlPlugin;
095    private String _exportCSVUrl;
096    private String _exportCSVUrlPlugin;
097    private String _exportDOCUrl;
098    private String _exportDOCUrlPlugin;
099    private String _exportXMLUrl;
100    private String _exportXMLUrlPlugin;
101    private String _exportPDFUrl;
102    private String _exportPDFUrlPlugin;
103    private String _printUrl;
104    private String _printUrlPlugin;
105    private String _summaryView;
106    private boolean _sortOnMultipleJoin;
107    
108    @Override
109    public void contextualize(Context context) throws ContextException
110    {
111        _context = context;
112    }
113    
114    public void service(ServiceManager manager) throws ServiceException
115    {
116        _manager = manager;
117        
118        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
119        _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE);
120        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
121        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
122        _searchCriterionHelper = (SearchCriterionHelper) manager.lookup(SearchCriterionHelper.ROLE);
123    }
124    
125    @Override
126    public void configure(Configuration configuration) throws ConfigurationException
127    {
128        try
129        {
130            _searchCriterionManager = new ThreadSafeComponentManager<>();
131            _searchCriterionManager.setLogger(getLogger());
132            _searchCriterionManager.contextualize(_context);
133            _searchCriterionManager.service(_manager);
134            
135            Configuration searchConfig = configuration.getChild("SearchModel");
136//            _cTypes = _configureContentTypes(searchConfig.getChild("content-types", false));
137            _configureContentTypes(searchConfig.getChild("content-types", false));
138            
139            _summaryView = searchConfig.getChild("summary-view").getValue(null);
140            _sortOnMultipleJoin = searchConfig.getChild("allow-sort-on-multiple-join").getValueAsBoolean(super.allowSortOnMultipleJoin());
141            
142            // Get the base content types and try to find common ancestors.
143            Set<String> baseCTypeIds = _configureBaseContentTypes(searchConfig.getChild("content-types"));
144            Set<ContentType> commonContentTypes = _contentTypesHelper.getCommonAncestors(baseCTypeIds)
145                    .stream()
146                    .map(_contentTypeExtensionPoint::getExtension)
147                    .collect(Collectors.toSet());
148            
149            _pageSize = searchConfig.getChild("page-size").getValueAsInteger(50);
150            if (_pageSize < 0)
151            {
152                _pageSize = 50;
153            }
154            _workspace = searchConfig.getChild("workspace").getValue(null);
155            
156            _configureSearchUrl(searchConfig);
157            _configureExportCSVUrl(searchConfig);
158            _configureExportDOCUrl(searchConfig);
159            _configureExportXMLUrl(searchConfig);
160            _configureExportPDFUrl(searchConfig);
161            _configurePrintUrl(searchConfig);
162            
163            _criteriaIndex = 0;
164            List<String> searchCriteriaRoles = new ArrayList<>();
165            List<String> facetedSearchUICriterionRoles = new ArrayList<>();
166            List<String> advancedSearchUICriterionRoles = new ArrayList<>();
167            
168            _addCriteriaComponents(commonContentTypes, searchConfig.getChild("simple-search-criteria"), searchCriteriaRoles);
169            
170            Configuration advancedCriteriaConf = searchConfig.getChild("advanced-search-criteria", false);
171            if (advancedCriteriaConf != null)
172            {
173                _addCriteriaComponents(commonContentTypes, advancedCriteriaConf, advancedSearchUICriterionRoles);
174            }
175            
176            Configuration facetsConf = searchConfig.getChild("facets", false);
177            if (facetsConf != null)
178            {
179                _addFacetCriteriaComponents(commonContentTypes, facetsConf, facetedSearchUICriterionRoles);
180            }
181            
182            _searchCriterionManager.initialize();
183            
184            _searchCriteria = new LinkedHashMap<>();
185            _facetedCriteria = new LinkedHashMap<>();
186            _advancedSearchCriteria = new LinkedHashMap<>();
187            
188            _configureCriteria(_searchCriteria, searchCriteriaRoles, false);
189            if (advancedCriteriaConf != null)
190            {
191                if (advancedSearchUICriterionRoles.isEmpty())
192                {
193                    // The facet root configuration is present but has no criteria configuration:
194                    // copy the simple search criteria.
195                    _copyAdvancedCriteria(_advancedSearchCriteria, _searchCriteria.values());
196                }
197                else
198                {
199                    _configureCriteria(_advancedSearchCriteria, advancedSearchUICriterionRoles, false);
200                }
201            }
202            
203            if (facetsConf != null)
204            {
205                if (facetedSearchUICriterionRoles.isEmpty())
206                {
207                    // The facet root configuration is present but has no criteria configuration:
208                    // copy the simple search criteria which are facetable.
209                    _copyFacetableCriteria(_facetedCriteria, _searchCriteria.values());
210                }
211                else
212                {
213                    _configureCriteria(_facetedCriteria, facetedSearchUICriterionRoles, true);
214                }
215            }
216            
217            Configuration columnConfs = searchConfig.getChild("columns").getChild("default");
218            ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes);
219            try
220            {
221                LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null);
222                _resultItems = parser.parseView(new ConfigurationAndPluginName(columnConfs, ""));
223            }
224            catch (Exception e)
225            {
226                throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e);
227            }
228            finally
229            {
230                LifecycleHelper.dispose(parser);
231            }
232        }
233        catch (Exception e)
234        {
235            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
236        }
237    }
238    
239    @Override
240    public void dispose()
241    {
242        _searchCriterionManager.dispose();
243        _searchCriterionManager = null;
244    }
245    
246    @Override
247    public Set<String> getContentTypes(Map<String, Object> contextualParameters)
248    {
249        return Collections.unmodifiableSet(_cTypes);
250    }
251    
252    @Override
253    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
254    {
255        return Collections.unmodifiableSet(_excludedCTypes);
256    }
257    
258    @Override
259    public int getPageSize(Map<String, Object> contextualParameters)
260    {
261        return _pageSize;
262    }
263    
264    @Override
265    public String getWorkspace(Map<String, Object> contextualParameters)
266    {
267        return _workspace;
268    }
269    
270    @Override
271    public String getSearchUrl(Map<String, Object> contextualParameters)
272    {
273        return _searchUrl;
274    }
275    
276    @Override
277    public String getSearchUrlPlugin(Map<String, Object> contextualParameters)
278    {
279        return _searchUrlPlugin;
280    }
281
282    @Override
283    public String getExportCSVUrl(Map<String, Object> contextualParameters)
284    {
285        return _exportCSVUrl;
286    }
287    
288    @Override
289    public String getExportCSVUrlPlugin(Map<String, Object> contextualParameters)
290    {
291        return _exportCSVUrlPlugin;
292    }
293
294    @Override
295    public String getExportDOCUrl(Map<String, Object> contextualParameters)
296    {
297        return _exportDOCUrl;
298    }
299
300    @Override
301    public String getExportDOCUrlPlugin(Map<String, Object> contextualParameters)
302    {
303        return _exportDOCUrlPlugin;
304    }
305
306    @Override
307    public String getExportXMLUrl(Map<String, Object> contextualParameters)
308    {
309        return _exportXMLUrl;
310    }
311
312    @Override
313    public String getExportXMLUrlPlugin(Map<String, Object> contextualParameters)
314    {
315        return _exportXMLUrlPlugin;
316    }
317
318    @Override
319    public String getExportPDFUrl(Map<String, Object> contextualParameters)
320    {
321        return _exportPDFUrl;
322    }
323    
324    @Override
325    public String getExportPDFUrlPlugin(Map<String, Object> contextualParameters)
326    {
327        return _exportPDFUrlPlugin;
328    }
329    
330    @Override
331    public String getPrintUrl(Map<String, Object> contextualParameters)
332    {
333        return _printUrl;
334    }
335    
336    @Override
337    public String getPrintUrlPlugin(Map<String, Object> contextualParameters)
338    {
339        return _printUrlPlugin;
340    }
341    
342    @Override
343    public String getSummaryView()
344    {
345        return _summaryView;
346    }
347    
348    @Override
349    public boolean allowSortOnMultipleJoin()
350    {
351        return _sortOnMultipleJoin;
352    }
353
354//    @Override
355//    public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters)
356//    {
357//        return Collections.unmodifiableMap(_searchCriteria);
358//    }
359//    
360//    @Override
361//    public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters)
362//    {
363//        return Collections.unmodifiableMap(_facetedCriteria);
364//    }
365//    
366//    @Override
367//    public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters)
368//    {
369//        return Collections.unmodifiableMap(_advancedSearchCriteria);
370//    }
371    
372    /**
373     * Configure the content type ids
374     * @param configuration The content types configuration
375     * @throws ConfigurationException If an error occurs
376     */
377//    protected Set<String> _configureContentTypes(Configuration configuration) throws ConfigurationException
378    protected void _configureContentTypes(Configuration configuration) throws ConfigurationException
379    {
380        _cTypes = new HashSet<>();
381        _excludedCTypes = new HashSet<>();
382        
383        if (configuration != null)
384        {
385            Configuration excludeConf = configuration.getChild("exclude");
386            
387            List<String> excludedTags = new ArrayList<>();
388            for (Configuration tagCong : excludeConf.getChildren("tag"))
389            {
390                excludedTags.add(tagCong.getValue());
391            }
392            
393            List<String> excludedCTypes = new ArrayList<>();
394            for (Configuration cType : excludeConf.getChildren("content-type"))
395            {
396                excludedCTypes.add(cType.getValue());
397            }
398            
399            Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
400            if (cTypesConfiguration.length == 0)
401            {
402                // Keep "content types" empty.
403                for (String id : _contentTypeExtensionPoint.getExtensionsIds())
404                {
405                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
406                    {
407                        _excludedCTypes.add(id);
408                    }
409                }
410            }
411            else
412            {
413                for (Configuration conf : configuration.getChildren("content-type"))
414                {
415                    String id = conf.getAttribute("id");
416                    _cTypes.add(id);
417                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
418                    {
419                        _excludedCTypes.add(id);
420                    }
421                    
422                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id))
423                    {
424                        if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes))
425                        {
426                            _excludedCTypes.add(subTypeId);
427                        }
428                    }
429                }
430            }
431        }
432    }
433    
434    /**
435     * Configure the base content type ids.
436     * @param configuration The content types configuration
437     * @return The set of base content type ids
438     * @throws ConfigurationException If an error occurs
439     */
440    protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
441    {
442        Set<String> cTypes = new HashSet<>();
443        
444        Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
445        if (cTypesConfiguration.length == 0)
446        {
447            cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds());
448        }
449        else
450        {
451            for (Configuration conf : cTypesConfiguration)
452            {
453                cTypes.add(conf.getAttribute("id"));
454            }
455        }
456        
457        return cTypes;
458    }
459    
460    /**
461     * Determines if the content type is a valid content type in current configuration 
462     * @param id The content type id
463     * @param excludedTags The tags to exclude
464     * @param excludedContentTypes The content types to exclude
465     * @return <code>true</code> if the content type is a valid content type
466     */
467    protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes)
468    {
469        if (excludedContentTypes.contains(id))
470        {
471            return false;
472        }
473        
474        ContentType cType = _contentTypeExtensionPoint.getExtension(id);
475        for (String tag : excludedTags)
476        {
477            if (cType.hasTag(tag))
478            {
479                return false;
480            }
481        }
482        
483        return true;
484    }
485    
486    private void _configureSearchUrl(Configuration configuration)
487    {
488        _searchUrlPlugin = configuration.getChild("search-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
489        _searchUrl = configuration.getChild("search-url").getValue(DEFAULT_SEARCH_URL);
490    }
491    
492    private void _configureExportCSVUrl(Configuration configuration)
493    {
494        _exportCSVUrlPlugin = configuration.getChild("export-csv-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
495        _exportCSVUrl = configuration.getChild("export-csv-url").getValue(DEFAULT_EXPORT_CSV_URL);
496    }
497    
498    private void _configureExportDOCUrl(Configuration configuration)
499    {
500        _exportDOCUrlPlugin = configuration.getChild("export-doc-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
501        _exportDOCUrl = configuration.getChild("export-doc-url").getValue(DEFAULT_EXPORT_DOC_URL);
502    }
503
504    private void _configureExportXMLUrl(Configuration configuration)
505    {
506        _exportXMLUrlPlugin = configuration.getChild("export-xml-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
507        _exportXMLUrl = configuration.getChild("export-xml-url").getValue(DEFAULT_EXPORT_XML_URL);
508    }
509
510    private void _configureExportPDFUrl(Configuration configuration)
511    {
512        _exportPDFUrlPlugin = configuration.getChild("export-pdf-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
513        _exportPDFUrl = configuration.getChild("export-pdf-url").getValue(DEFAULT_EXPORT_PDF_URL);
514    }
515    
516    private void _configurePrintUrl(Configuration configuration)
517    {
518        _printUrlPlugin = configuration.getChild("print-url").getAttribute("plugin", DEFAULT_URL_PLUGIN);
519        _printUrl = configuration.getChild("print-url").getValue(DEFAULT_PRINT_URL);
520    }
521    
522    /**
523     * Add criteria components to the search criteria manager.
524     * @param commonContentTypes the model's common content types.
525     * @param configuration the model configuration.
526     * @param searchCriteriaRoles the criteria role list to fill.
527     * @throws ConfigurationException if an error occurs.
528     */
529    protected void _addCriteriaComponents(Set<ContentType> commonContentTypes, Configuration configuration, List<String> searchCriteriaRoles) throws ConfigurationException
530    {
531        for (Configuration groupConf : configuration.getChildren("group"))
532        {
533            I18nizableText group = _configureI18nizableText(groupConf.getChild("label", false), null);
534            
535            _addCriteriaComponents (commonContentTypes, groupConf, searchCriteriaRoles, group);
536        }
537        
538        // Criteria without groups
539        _addCriteriaComponents (commonContentTypes, configuration, searchCriteriaRoles, null);
540    }
541    
542    /**
543     * Add standard criteria components to the search criteria manager.
544     * @param commonContentTypes the model's common content types.
545     * @param configuration the model configuration.
546     * @param searchCriteriaRoles the criteria role list to fill.
547     * @param group the criteria group.
548     * @throws ConfigurationException if an error occurs.
549     */
550    protected void _addCriteriaComponents(Set<ContentType> commonContentTypes, Configuration configuration, List<String> searchCriteriaRoles, I18nizableText group) throws ConfigurationException
551    {
552        for (Configuration conf : configuration.getChildren("criteria"))
553        {
554            String fieldRef = conf.getAttribute("field-ref", null);
555            String systemProperty = conf.getAttribute("system-ref", null);
556            String customId = conf.getAttribute("custom-ref", null);
557            
558            if (StringUtils.isNotEmpty(fieldRef))
559            {
560                _addIndexingFieldCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, fieldRef, group);
561            }
562            else if (systemProperty != null)
563            {
564                _addSystemCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, systemProperty, group);
565            }
566            else if (customId != null)
567            {
568                _addCustomCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, customId, group);
569            }
570        }
571    }
572    
573    /**
574     * Add facet criteria components to the search criteria manager.
575     * @param commonContentTypes the model's common content types.
576     * @param configuration the model configuration.
577     * @param searchCriteriaRoles the criteria role list to fill.
578     * @throws ConfigurationException if an error occurs.
579     */
580    protected void _addFacetCriteriaComponents(Set<ContentType> commonContentTypes, Configuration configuration, List<String> searchCriteriaRoles) throws ConfigurationException
581    {
582        for (Configuration conf : configuration.getChildren("criteria"))
583        {
584            String fieldRef = conf.getAttribute("field-ref", null);
585            String systemProperty = conf.getAttribute("system-ref", null);
586            String customId = conf.getAttribute("custom-ref", null);
587            
588            if (StringUtils.isNotEmpty(fieldRef))
589            {
590                _addIndexingFieldCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, fieldRef, null);
591            }
592            else if (systemProperty != null)
593            {
594                _addSystemCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, systemProperty, null);
595            }
596            else if (customId != null)
597            {
598                _addCustomCriteriaComponents(commonContentTypes, conf, searchCriteriaRoles, customId, null);
599            }
600        }
601    }
602    
603    /**
604     * Add a indexing field criteria component to the manager.
605     * @param commonContentTypes the model common content types.
606     * @param conf the criteria configuration.
607     * @param searchCriteriaRoles the criteria role list to fill.
608     * @param fieldRef the field path.
609     * @param group The group.
610     * @throws ConfigurationException if an error occurs.
611     */
612    protected void _addIndexingFieldCriteriaComponents(Set<ContentType> commonContentTypes, Configuration conf, List<String> searchCriteriaRoles, String fieldRef, I18nizableText group) throws ConfigurationException
613    {
614        try
615        {
616            if (commonContentTypes.isEmpty() && (fieldRef.equals("*") || fieldRef.equals(Content.ATTRIBUTE_TITLE)))
617            {
618                // If no common ancestors, only title attribute is allowed
619                String role = Content.ATTRIBUTE_TITLE + _criteriaIndex;
620                _criteriaIndex++;
621                Configuration criteriaConf = _searchCriterionHelper.getIndexingFieldCriteriaConfiguration(this, Optional.ofNullable(conf), Set.of(), Content.ATTRIBUTE_TITLE, Optional.of(Operator.EQ), Optional.ofNullable(group));
622                
623                _searchCriterionManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
624                
625                searchCriteriaRoles.add(role);
626            }
627            else if (!commonContentTypes.isEmpty() && fieldRef.equals("*"))
628            {
629                Set<String> commonContentTypeIds = commonContentTypes.stream()
630                        .map(ContentType::getId)
631                        .collect(Collectors.toSet());
632                
633                for (ContentType commonContentType : commonContentTypes)
634                {
635                    for (ModelItem modelItem : commonContentType.getModelItems())
636                    {
637                        // Get only first-level field (ignore composites and repeaters)
638                        if (modelItem instanceof ElementDefinition definition)
639                        {
640                            String role = definition.getName() + _criteriaIndex;
641                            _criteriaIndex++;
642                            Configuration criteriaConf = _searchCriterionHelper.getIndexingFieldCriteriaConfiguration(this, Optional.ofNullable(conf), commonContentTypeIds, definition.getName(), Optional.empty(), Optional.ofNullable(group));
643                            
644                            _searchCriterionManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
645                            
646                            searchCriteriaRoles.add(role);
647                        }
648                    }
649                }
650            }
651            else if (!commonContentTypes.isEmpty())
652            {
653                Set<String> commonContentTypeIds = commonContentTypes.stream()
654                        .map(ContentType::getId)
655                        .collect(Collectors.toSet());
656                
657                // The field ref is the indexing field path.
658                String role = fieldRef + _criteriaIndex;
659                _criteriaIndex++;
660                Configuration criteriaConf = _searchCriterionHelper.getIndexingFieldCriteriaConfiguration(this, Optional.ofNullable(conf), commonContentTypeIds, fieldRef, Optional.empty(), Optional.ofNullable(group));
661                
662                _searchCriterionManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
663                
664                searchCriteriaRoles.add(role);
665            }
666        }
667        catch (Exception e)
668        {
669            throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, conf, e);
670        }
671    }
672    
673    /**
674     * Add a system criteria component to the manager.
675     * @param commonContentTypes the model common content types.
676     * @param originalConf the criteria configuration.
677     * @param searchCriteriaRoles the criteria role list to fill.
678     * @param property the system property id.
679     * @param group The group.
680     * @throws ConfigurationException if an error occurs.
681     */
682    protected void _addSystemCriteriaComponents(Set<ContentType> commonContentTypes, Configuration originalConf, List<String> searchCriteriaRoles, String property, I18nizableText group) throws ConfigurationException
683    {
684        try
685        {
686            Set<String> commonContentTypeIds = commonContentTypes.stream()
687                    .map(ContentType::getId)
688                    .collect(Collectors.toSet());
689            
690            if (property.equals("*"))
691            {
692                for (String propId : _systemPropertyExtensionPoint.getSearchProperties())
693                {
694                    String role = propId + _criteriaIndex;
695                    _criteriaIndex++;
696                    
697                    Configuration criteriaConf = _searchCriterionHelper.getSystemCriteriaConfiguration(this, Optional.ofNullable(originalConf), commonContentTypeIds, propId, Optional.ofNullable(group));
698                    _searchCriterionManager.addComponent("cms", null, role, SystemSearchUICriterion.class, criteriaConf);
699                    
700                    searchCriteriaRoles.add(role);
701                }
702            }
703            else
704            {
705                String role = property + _criteriaIndex;
706                _criteriaIndex++;
707                
708                Configuration criteriaConf = _searchCriterionHelper.getSystemCriteriaConfiguration(this, Optional.ofNullable(originalConf), commonContentTypeIds, property, Optional.ofNullable(group));
709                _searchCriterionManager.addComponent("cms", null, role, SystemSearchUICriterion.class, criteriaConf);
710                
711                searchCriteriaRoles.add(role);
712            }
713        }
714        catch (Exception e)
715        {
716            throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, originalConf, e);
717        }
718    }
719    
720    /**
721     * Add a custom criteria component to the manager.
722     * @param commonContentTypes the model common content types.
723     * @param conf the criteria configuration.
724     * @param searchCriteriaRoles the criteria role list to fill.
725     * @param searchCriterionId the custom criteria id.
726     * @param group The group. Can be null.
727     * @throws ConfigurationException if an error occurs.
728     */
729    protected void _addCustomCriteriaComponents(Set<ContentType> commonContentTypes, Configuration conf, List<String> searchCriteriaRoles, String searchCriterionId, I18nizableText group) throws ConfigurationException
730    {
731        Configuration classConf = conf.getChild("class");
732        String className = classConf.getAttribute("name", null);
733        
734        if (className == null)
735        {
736            throw new ConfigurationException("The custom search criterion '" + searchCriterionId + "' does not specifiy a class.", conf);
737        }
738        
739        try
740        {
741            String role = searchCriterionId + _criteriaIndex;
742            _criteriaIndex++;
743            
744            // Common content type or first content type
745            String defaultContentTypeId = _searchCriterionHelper.getAllContentTypes(this, Map.of()).iterator().next();
746            Set<String> contentTypeIds = commonContentTypes.isEmpty() ? Set.of(defaultContentTypeId) : commonContentTypes.stream()
747                    .map(ContentType::getId)
748                    .collect(Collectors.toSet());
749            
750            Configuration criteriaConf = _searchCriterionHelper.getCustomCriteriaConfiguration(this, Optional.ofNullable(conf), contentTypeIds, searchCriterionId, Optional.ofNullable(group));
751            
752            @SuppressWarnings("unchecked")
753            Class<SearchUICriterion> searchCriteriaClass = (Class<SearchUICriterion>) Class.forName(className);
754            _searchCriterionManager.addComponent("cms", null, role, searchCriteriaClass, criteriaConf);
755            
756            searchCriteriaRoles.add(role);
757        }
758        catch (Exception e)
759        {
760            throw new ConfigurationException("Unable to instanciate custom SearchUICriterion for class: " + className, conf, e);
761        }
762    }
763    
764    /**
765     * Lookup the previously initialized criteria components and fill the given map with them.
766     * @param criteriaMap the criteria map to fill.
767     * @param criteriaRoles the roles of the criteria components to lookup.
768     * @param checkFacetable true to check if the criteria are facetable.
769     * @throws ConfigurationException if an error occurs.
770     */
771    protected void _configureCriteria(Map<String, SearchUICriterion> criteriaMap, List<String> criteriaRoles, boolean checkFacetable) throws ConfigurationException
772    {
773        for (String role : criteriaRoles)
774        {
775            try
776            {
777                SearchUICriterion criterion = _searchCriterionManager.lookup(role);
778                
779                if (checkFacetable && !criterion.isFacetable())
780                {
781                    throw new ConfigurationException("The search criteria of id '" + criterion.getId() + "' is not facetable.");
782                }
783                
784                criteriaMap.put(criterion.getId(), criterion);
785            }
786            catch (ComponentException e)
787            {
788                throw new ConfigurationException("Impossible to lookup the search criteria of role: " + role, e);
789            }
790        }
791    }
792    
793    /**
794     * Copy all the allowed search criteria to the given criteria map.
795     * @param advancedCriteria the criteria map to fill.
796     * @param criteria the source criteria collection.
797     * @throws ConfigurationException if an error occurs.
798     */
799    protected void _copyAdvancedCriteria(Map<String, SearchUICriterion> advancedCriteria, Collection<SearchUICriterion> criteria) throws ConfigurationException
800    {
801        for (SearchUICriterion criterion : criteria)
802        {
803            if (_isAdvanced(criterion))
804            {
805                advancedCriteria.put(criterion.getId(), criterion);
806            }
807        }
808    }
809    
810    /**
811     * Copy all the facetable search criteria to the given criteria map.
812     * @param facetedCriteria the criteria map to fill.
813     * @param criteria the source criteria collection.
814     * @throws ConfigurationException if an error occurs.
815     */
816    protected void _copyFacetableCriteria(Map<String, SearchUICriterion> facetedCriteria, Collection<SearchUICriterion> criteria) throws ConfigurationException
817    {
818        for (SearchUICriterion criterion : criteria)
819        {
820            if (criterion.isFacetable())
821            {
822                facetedCriteria.put(criterion.getId(), criterion);
823            }
824        }
825    }
826    
827    /**
828     * Test if a search criterion can be used in advanced search mode.
829     * For instance: geocode, rich-text, file-typed criterion are not allowed.
830     * @param criterion the search criterion to test.
831     * @return <code>true</code> if the criterion can be used in advanced search mode, <code>false</code> otherwise.
832     */
833    protected boolean _isAdvanced(SearchUICriterion criterion)
834    {
835        boolean isAdvanced = false;
836        
837        switch (criterion.getType())
838        {
839            case STRING:
840            case MULTILINGUAL_STRING:
841            case LONG:
842            case DOUBLE:
843            case DATE:
844            case DATETIME:
845            case BOOLEAN:
846            case CONTENT:
847            case USER:
848            case RICH_TEXT:
849                isAdvanced = true;
850                break;
851            case COMPOSITE:
852            case FILE:
853            case BINARY:
854            case REFERENCE:
855            case GEOCODE:
856            default:
857                // Do nothing.
858                break;
859        }
860        
861        return isAdvanced;
862    }
863    
864    private I18nizableText _configureI18nizableText(Configuration config, I18nizableText defaultValue) throws ConfigurationException
865    {
866        if (config != null)
867        {
868            return I18nizableText.parseI18nizableText(config, null);
869        }
870        else
871        {
872            return defaultValue;
873        }
874    }
875    
876}