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