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