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