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