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