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.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.function.Predicate;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.activity.Disposable;
029import org.apache.avalon.framework.component.ComponentException;
030import org.apache.avalon.framework.configuration.Configurable;
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.cocoon.components.LifecycleHelper;
040import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
041import org.apache.commons.lang3.StringUtils;
042import org.slf4j.Logger;
043
044import org.ametys.cms.contenttype.ContentType;
045import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
046import org.ametys.cms.contenttype.ContentTypesHelper;
047import org.ametys.cms.data.type.ModelItemTypeConstants;
048import org.ametys.cms.model.properties.Property;
049import org.ametys.cms.repository.Content;
050import org.ametys.cms.search.model.CriterionDefinitionAwareElementDefinition;
051import org.ametys.cms.search.model.CriterionDefinitionHelper;
052import org.ametys.cms.search.model.IndexationAwareElementDefinition;
053import org.ametys.cms.search.model.SearchModelCriterionDefinition;
054import org.ametys.cms.search.model.SearchModelCriterionDefinitionHelper;
055import org.ametys.cms.search.model.SystemProperty;
056import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
057import org.ametys.cms.search.model.impl.ReferencingSearchModelCriterionDefinition;
058import org.ametys.cms.search.ui.model.impl.DefaultSearchModelCriterionViewItem;
059import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel;
060import org.ametys.runtime.model.ElementDefinition;
061import org.ametys.runtime.model.ItemParserHelper;
062import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName;
063import org.ametys.runtime.model.ModelItem;
064import org.ametys.runtime.model.ModelViewItem;
065import org.ametys.runtime.model.SimpleViewItemGroup;
066import org.ametys.runtime.model.View;
067import org.ametys.runtime.model.ViewItem;
068import org.ametys.runtime.model.ViewItemAccessor;
069import org.ametys.runtime.model.ViewItemContainer;
070import org.ametys.runtime.model.ViewItemGroup;
071import org.ametys.runtime.model.ViewParser;
072import org.ametys.runtime.model.type.DataContext;
073import org.ametys.runtime.plugin.component.LogEnabled;
074import org.ametys.runtime.plugin.component.PluginAware;
075import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
076
077/**
078 * Static implementation of a {@link SearchUIModel}
079 */
080public class StaticSearchUIModel extends DefaultSearchUIModel implements Serviceable, Contextualizable, Configurable, Disposable, LogEnabled, PluginAware
081{
082    /** ComponentManager for {@link SearchModelCriterionDefinition}s. */
083    protected ThreadSafeComponentManager<SearchModelCriterionDefinition> _criterionDefinitionManager;
084    
085    /** The context. */
086    protected Context _context;
087    
088    /** The service manager */
089    protected ServiceManager _manager;
090    
091    /** The plugin name */
092    protected String _pluginName;
093    
094    /** The content type helper. */
095    protected ContentTypesHelper _contentTypesHelper;
096    
097    /** The helper for columns */
098    protected ColumnHelper _columnHelper;
099    
100    /** The content type extension point */
101    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
102    
103    /** The system property extension point. */
104    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
105    
106    /** The criterion definition helper */
107    protected CriterionDefinitionHelper _criterionDefinitionHelper;
108    
109    /** The helper for search model criterion definition */
110    protected SearchModelCriterionDefinitionHelper _searchModelCriterionDefinitionHelper;
111    
112    /** The helper for search model criterion definition */
113    protected SearchModelCriterionViewItemHelper _searchUIModelCriterionDefinitionHelper;
114    
115    /** The logger. */
116    protected Logger _logger;
117    
118    /** The search model identifier */
119    protected String _id;
120    
121    private int _criteriaIndex;
122
123    public void setLogger(final Logger logger)
124    {
125        _logger = logger;
126    }
127    
128    /**
129     * Get the logger.
130     * @return the logger.
131     */
132    protected final Logger getLogger()
133    {
134        return _logger;
135    }
136    
137    @Override
138    public void contextualize(Context context) throws ContextException
139    {
140        _context = context;
141    }
142    
143    public void service(ServiceManager manager) throws ServiceException
144    {
145        _manager = manager;
146        
147        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
148        _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE);
149        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
150        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
151        _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE);
152        _searchModelCriterionDefinitionHelper = (SearchModelCriterionDefinitionHelper) manager.lookup(SearchModelCriterionDefinitionHelper.ROLE);
153        _searchUIModelCriterionDefinitionHelper = (SearchModelCriterionViewItemHelper) manager.lookup(SearchModelCriterionViewItemHelper.ROLE);
154    }
155
156    public void setPluginInfo(String pluginName, String featureName, String id)
157    {
158        _pluginName = pluginName;
159        _id = id;
160    }
161    
162    @Override
163    public void configure(Configuration configuration) throws ConfigurationException
164    {
165        try
166        {
167            _criterionDefinitionManager = new ThreadSafeComponentManager<>();
168            _criterionDefinitionManager.setLogger(getLogger());
169            _criterionDefinitionManager.contextualize(_context);
170            _criterionDefinitionManager.service(_manager);
171            
172            Configuration searchConfig = configuration.getChild("SearchModel");
173            _configureContentTypes(searchConfig.getChild("content-types", false));
174            
175            setSummaryView(searchConfig.getChild("summary-view").getValue(null));
176            setAllowSortOnMultipleJoin(searchConfig.getChild("allow-sort-on-multiple-join").getValueAsBoolean(super.allowSortOnMultipleJoin()));
177            
178            // Get the base content types and try to find common ancestors.
179            Set<String> baseCTypeIds = _configureBaseContentTypes(searchConfig.getChild("content-types"));
180            Set<ContentType> commonContentTypes = _contentTypesHelper.getCommonAncestors(baseCTypeIds)
181                    .stream()
182                    .map(_contentTypeExtensionPoint::getExtension)
183                    .collect(Collectors.toSet());
184            
185            int pageSize = searchConfig.getChild("page-size").getValueAsInteger(50);
186            if (pageSize < 0)
187            {
188                pageSize = 50;
189            }
190            setPageSize(pageSize);
191            setWorkspace(searchConfig.getChild("workspace").getValue(null));
192            
193            _configureSearchUrl(searchConfig);
194            _configureExportCSVUrl(searchConfig);
195            _configureExportDOCUrl(searchConfig);
196            _configureExportXMLUrl(searchConfig);
197            _configureExportPDFUrl(searchConfig);
198            _configurePrintUrl(searchConfig);
199            
200            _criteriaIndex = 0;
201            Map<String, ModelViewItem> criteriaRoles = new HashMap<>();
202            Map<String, ModelViewItem> advancedCriteriaRoles = new HashMap<>();
203            Map<String, ModelViewItem> facetedCriteriaRoles = new HashMap<>();
204            
205            Configuration criteriaConf = searchConfig.getChild("simple-search-criteria");
206            ViewItemContainer criteria = _parseCriteria(commonContentTypes, criteriaConf, criteriaRoles, true);
207            
208            Configuration advancedCriteriaConf = searchConfig.getChild("advanced-search-criteria", false);
209            ViewItemContainer advancedCriteria = new View();
210            if (advancedCriteriaConf != null)
211            {
212                advancedCriteria = _parseCriteria(commonContentTypes, advancedCriteriaConf, advancedCriteriaRoles, false);
213            }
214            
215            Configuration facetsConf = searchConfig.getChild("facets", false);
216            ViewItemContainer facetedCriteria = new View();
217            if (facetsConf != null)
218            {
219                facetedCriteria = _parseCriteria(commonContentTypes, facetsConf, facetedCriteriaRoles, false);
220            }
221            
222            _criterionDefinitionManager.initialize();
223            
224            _lookupCriteriaComponents(criteriaRoles, false);
225            if (advancedCriteriaConf != null)
226            {
227                if (advancedCriteria.getViewItems().isEmpty())
228                {
229                    // The advanced criteria root configuration is present but has no criteria:
230                    // copy the simple criteria.
231                    advancedCriteria = _copyCriteria(criteria, this::_isAdvanced);
232                }
233                else
234                {
235                    _lookupCriteriaComponents(advancedCriteriaRoles, false);
236                }
237            }
238            
239            if (facetsConf != null)
240            {
241                if (facetedCriteria.getViewItems().isEmpty())
242                {
243                    // The facet root configuration is present but has no criteria:
244                    // copy the simple criteria which are facetable.
245                    facetedCriteria = _copyCriteria(criteria, this::_isFacetable);
246                }
247                else
248                {
249                    _lookupCriteriaComponents(facetedCriteriaRoles, true);
250                }
251            }
252            
253            setCriteria(criteria);
254            setAdvancedCriteria(advancedCriteria);
255            setFacetedCriteria(facetedCriteria);
256            
257            Configuration columnConfs = searchConfig.getChild("columns").getChild("default");
258            ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes);
259            try
260            {
261                LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null);
262                setResultItems(parser.parseView(new ConfigurationAndPluginName(columnConfs, "")));
263            }
264            catch (Exception e)
265            {
266                throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e);
267            }
268            finally
269            {
270                LifecycleHelper.dispose(parser);
271            }
272        }
273        catch (Exception e)
274        {
275            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
276        }
277    }
278    
279    @Override
280    public void dispose()
281    {
282        _criterionDefinitionManager.dispose();
283        _criterionDefinitionManager = null;
284    }
285    
286    /**
287     * Configure the content type ids
288     * @param configuration The content types configuration
289     * @throws ConfigurationException If an error occurs
290     */
291    protected void _configureContentTypes(Configuration configuration) throws ConfigurationException
292    {
293        Set<String> contentTypes = new HashSet<>();
294        Set<String> excludedContentTypes = new HashSet<>();
295        
296        if (configuration != null)
297        {
298            Configuration excludeConf = configuration.getChild("exclude");
299            
300            List<String> excludedTags = new ArrayList<>();
301            for (Configuration tagCong : excludeConf.getChildren("tag"))
302            {
303                excludedTags.add(tagCong.getValue());
304            }
305            
306            List<String> excludedCTypes = new ArrayList<>();
307            for (Configuration cType : excludeConf.getChildren("content-type"))
308            {
309                excludedCTypes.add(cType.getValue());
310            }
311            
312            Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
313            if (cTypesConfiguration.length == 0)
314            {
315                // Keep "content types" empty.
316                for (String id : _contentTypeExtensionPoint.getExtensionsIds())
317                {
318                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
319                    {
320                        excludedContentTypes.add(id);
321                    }
322                }
323            }
324            else
325            {
326                for (Configuration conf : configuration.getChildren("content-type"))
327                {
328                    String id = conf.getAttribute("id");
329                    contentTypes.add(id);
330                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
331                    {
332                        excludedContentTypes.add(id);
333                    }
334                    
335                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id))
336                    {
337                        if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes))
338                        {
339                            excludedContentTypes.add(subTypeId);
340                        }
341                    }
342                }
343            }
344        }
345        
346        setContentTypes(contentTypes);
347        setExcludedContentTypes(excludedContentTypes);
348    }
349    
350    /**
351     * Configure the base content type ids.
352     * @param configuration The content types configuration
353     * @return The set of base content type ids
354     * @throws ConfigurationException If an error occurs
355     */
356    protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
357    {
358        Set<String> cTypes = new HashSet<>();
359        
360        Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
361        if (cTypesConfiguration.length == 0)
362        {
363            cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds());
364        }
365        else
366        {
367            for (Configuration conf : cTypesConfiguration)
368            {
369                cTypes.add(conf.getAttribute("id"));
370            }
371        }
372        
373        return cTypes;
374    }
375    
376    /**
377     * Determines if the content type is a valid content type in current configuration 
378     * @param id The content type id
379     * @param excludedTags The tags to exclude
380     * @param excludedContentTypes The content types to exclude
381     * @return <code>true</code> if the content type is a valid content type
382     */
383    protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes)
384    {
385        if (excludedContentTypes.contains(id))
386        {
387            return false;
388        }
389        
390        ContentType cType = _contentTypeExtensionPoint.getExtension(id);
391        for (String tag : excludedTags)
392        {
393            if (cType.hasTag(tag))
394            {
395                return false;
396            }
397        }
398        
399        return true;
400    }
401    
402    private void _configureSearchUrl(Configuration configuration)
403    {
404        setSearchUrlPlugin(configuration.getChild("search-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
405        setSearchUrl(configuration.getChild("search-url").getValue(__DEFAULT_SEARCH_URL));
406    }
407    
408    private void _configureExportCSVUrl(Configuration configuration)
409    {
410        setExportCSVUrlPlugin(configuration.getChild("export-csv-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
411        setExportCSVUrl(configuration.getChild("export-csv-url").getValue(__DEFAULT_EXPORT_CSV_URL));
412    }
413    
414    private void _configureExportDOCUrl(Configuration configuration)
415    {
416        setExportDOCUrlPlugin(configuration.getChild("export-doc-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
417        setExportDOCUrl(configuration.getChild("export-doc-url").getValue(__DEFAULT_EXPORT_DOC_URL));
418    }
419
420    private void _configureExportXMLUrl(Configuration configuration)
421    {
422        setExportXMLUrlPlugin(configuration.getChild("export-xml-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
423        setExportXMLUrl(configuration.getChild("export-xml-url").getValue(__DEFAULT_EXPORT_XML_URL));
424    }
425
426    private void _configureExportPDFUrl(Configuration configuration)
427    {
428        setExportPDFUrlPlugin(configuration.getChild("export-pdf-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
429        setExportPDFUrl(configuration.getChild("export-pdf-url").getValue(__DEFAULT_EXPORT_PDF_URL));
430    }
431    
432    private void _configurePrintUrl(Configuration configuration)
433    {
434        setPrintUrlPlugin(configuration.getChild("print-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
435        setPrintUrl(configuration.getChild("print-url").getValue(__DEFAULT_PRINT_URL));
436    }
437    
438    /**
439     * Parses criteria in the given configuration.
440     * Add components in Search criteria manager and fill the Map with roles and {@link SearchModelCriterionViewItem}
441     * @param contentTypes the model's content types
442     * @param configuration the model's configuration.
443     * @param rolesByUICriteria the map to fill with the {@link SearchModelCriterionViewItem} and the role of the referenced {@link SearchModelCriterionDefinition}
444     * @param acceptGroups <code>true</code> if the parsed criteria is able to have groups, <code>false</code> otherwise
445     * @return the parsed criteria
446     * @throws ConfigurationException if an error occurs.
447     */
448    protected ViewItemContainer _parseCriteria(Set<ContentType> contentTypes, Configuration configuration, Map<String, ModelViewItem> rolesByUICriteria, boolean acceptGroups) throws ConfigurationException
449    {
450        ViewItemContainer criteria = new View();
451        ViewItemAccessor criteriaWithoutGroupAccessor = criteria;
452        
453        if (acceptGroups)
454        {
455            for (Configuration groupConf : configuration.getChildren("group"))
456            {
457                SimpleViewItemGroup group = new SimpleViewItemGroup();
458                group.setRole(groupConf.getAttribute("role", ViewItemGroup.FIELDSET_ROLE));
459                group.setName(groupConf.getAttribute("name", null));
460                
461                ConfigurationAndPluginName groupConfAndPluginName = new ConfigurationAndPluginName(groupConf, _pluginName);
462                group.setLabel(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "label"));
463                group.setDescription(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "description"));
464                
465                criteria.addViewItem(group);
466                
467                List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, groupConf);
468                group.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList());
469                uiCriteriaWithRoles.stream()
470                                   .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent())
471                                   .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem()));
472            }
473            
474            // Group for criteria with no group
475            SimpleViewItemGroup group = new SimpleViewItemGroup();
476            group.setRole(ViewItemGroup.FIELDSET_ROLE);
477            criteria.addViewItem(group);
478            criteriaWithoutGroupAccessor = group;
479        }
480        
481        // Criteria with no group
482        List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, configuration);
483        criteriaWithoutGroupAccessor.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList());
484        uiCriteriaWithRoles.stream()
485                           .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent())
486                           .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem()));
487        
488        return criteria;
489    }
490    
491    /**
492     * Parses criteria in the given configuration.
493     * Add components in Search criteria manager
494     * @param contentTypes the model's content types
495     * @param configuration the model's configuration.
496     * @return the Map with roles and {@link SearchModelCriterionViewItem}
497     * @throws ConfigurationException if an error occurs.
498     */
499    protected List<SearchModelCriterionViewItemWithRole> _parseCriteria(Set<ContentType> contentTypes, Configuration configuration) throws ConfigurationException
500    {
501        List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = new ArrayList<>();
502        
503        for (Configuration conf : configuration.getChildren("item"))
504        {
505            String reference = conf.getAttribute("ref", null);
506            
507            if (StringUtils.isNotEmpty(reference))
508            {
509                uiCriteriaWithRoles.addAll(_parseReferencingCriteria(contentTypes, conf, reference));
510            }
511            else
512            {
513                uiCriteriaWithRoles.addAll(_parseStaticCriteria(contentTypes, conf));
514            }
515        }
516        
517        return uiCriteriaWithRoles;
518    }
519    
520    /**
521     * Parses a criteria that references a model item of model's content types, or a system property
522     * Add a referencing criteria component to the manager.
523     * @param contentTypes the model's content types.
524     * @param conf the criteria configuration.
525     * @param reference the path of the referenced item
526     * @return A Map with the created UI criteria and potential role of the corresponding criterion
527     * @throws ConfigurationException if an error occurs.
528     */
529    protected List<SearchModelCriterionViewItemWithRole> _parseReferencingCriteria(Set<ContentType> contentTypes, Configuration conf, String reference) throws ConfigurationException
530    {
531        try
532        {
533            if (ViewParser.ALL_ITEMS_REFERENCE.equals(reference))
534            {
535                List<SearchModelCriterionViewItemWithRole> viewItemsWithRoles = new ArrayList<>();
536
537                if (contentTypes.isEmpty())
538                {
539                    // There is no given content types, only add the title attribute
540                    SearchModelCriterionViewItem titleCriterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, _contentTypesHelper.getTitleAttributeDefinition(), Content.ATTRIBUTE_TITLE);
541                    viewItemsWithRoles.add(new SearchModelCriterionViewItemWithRole(titleCriterionViewItem, Optional.empty()));
542                }
543                else
544                {
545                    // Add criteria for all model items of the given content types
546                    for (ContentType contentType : contentTypes)
547                    {
548                        for (ModelItem modelItem : contentType.getModelItems())
549                        {
550                            _getCriterionViewItemWithRole(modelItem).ifPresent(viewItemsWithRoles::add);
551                        }
552                    }
553                }
554                
555                // Add criteria for all system properties
556                for (String systemPropertyId : _systemPropertyExtensionPoint.getExtensionsIds())
557                {
558                    SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(systemPropertyId);
559                    _getCriterionViewItemWithRole(systemProperty).ifPresent(viewItemsWithRoles::add);
560                }
561                
562                return viewItemsWithRoles;
563            }
564            else
565            {
566                if (contentTypes.isEmpty() && !Content.ATTRIBUTE_TITLE.equals(reference) && !_systemPropertyExtensionPoint.hasExtension(reference))
567                {
568                    throw new ConfigurationException("The criteria '" + reference + "' is forbidden when no content type is specified: only title or system properties can be used.");
569                }
570                
571                // The criteria references a specific item
572                String role = reference + _criteriaIndex;
573                _criteriaIndex++;
574                SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf);
575                
576                Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes);
577                Class<? extends SearchModelCriterionDefinition> criterionDefinitionClass = _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference);
578                if (criterionDefinitionClass != null)
579                {
580                    _criterionDefinitionManager.addComponent("cms", null, role, _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference), wrapConf);
581                    return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role)));
582                }
583                else
584                {
585                    return List.of();
586                }
587            }
588        }
589        catch (Exception e)
590        {
591            throw new ConfigurationException("Unable to instanciate criterion definition referencing " + reference, conf, e);
592        }
593    }
594    
595    private Optional<SearchModelCriterionViewItemWithRole> _getCriterionViewItemWithRole(ModelItem modelItem)
596    {
597        if (modelItem instanceof ElementDefinition definition // Get only first-level field (ignore composites and repeaters)
598                && (!(definition instanceof Property) || definition instanceof CriterionDefinitionAwareElementDefinition)) // Exclude properties that are not criterion aware
599        {
600            SearchModelCriterionViewItem criterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, definition, definition.getName());
601            if (criterionViewItem != null)
602            {
603                return Optional.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.empty()));
604            }
605        }
606        
607        return Optional.empty();
608    }
609    
610    /**
611     * Parses a static criteria
612     * Add a referencing criteria component to the manager.
613     * @param contentTypes the model's content types.
614     * @param conf the criteria configuration.
615     * @return A Map with the created UI criteria and potential role of the corresponding criterion
616     * @throws ConfigurationException if an error occurs.
617     */
618    protected List<SearchModelCriterionViewItemWithRole> _parseStaticCriteria(Set<ContentType> contentTypes, Configuration conf) throws ConfigurationException
619    {
620        String criteriaName = conf.getAttribute("name", null);
621        String className = conf.getAttribute("class", null);
622        
623        if (criteriaName == null || className == null)
624        {
625            throw new ConfigurationException("The custom criterion definition '" + criteriaName + "' does not specifiy a class or a name.", conf);
626        }
627        
628        try
629        {
630            String role = criteriaName + _criteriaIndex;
631            _criteriaIndex++;
632            
633            SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf);
634            
635            @SuppressWarnings("unchecked")
636            Class<SearchModelCriterionDefinition> criterionDefinitionClass = (Class<SearchModelCriterionDefinition>) Class.forName(className);
637            Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes);
638            _criterionDefinitionManager.addComponent("cms", null, role, criterionDefinitionClass, wrapConf);
639            
640            return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role)));
641        }
642        catch (Exception e)
643        {
644            throw new ConfigurationException("Unable to instanciate custom criterion view item for class: " + className, conf, e);
645        }
646    }
647    
648    /**
649     * Parses the {@link SearchModelCriterionViewItem} from the given configuration
650     * @param configuration the configuration
651     * @return the parsed UI criterion
652     * @throws ConfigurationException if an error occurs
653     */
654    protected SearchModelCriterionViewItem _parseCriterionViewItem(Configuration configuration) throws ConfigurationException
655    {
656        SearchModelCriterionViewItem criterionViewItem = new DefaultSearchModelCriterionViewItem();
657        criterionViewItem.setHidden(configuration.getAttributeAsBoolean("hidden", false));
658        return criterionViewItem;
659    }
660    
661    /**
662     * Lookup the previously initialized criteria components and add the criteria to their pending {@link SearchModelCriterionViewItem}
663     * @param criteriaRoles the criteria map to fill.
664     * @param checkFacetable true to check if the criteria are facetable.
665     * @throws ConfigurationException if an error occurs.
666     */
667    protected void _lookupCriteriaComponents(Map<String, ModelViewItem> criteriaRoles, boolean checkFacetable) throws ConfigurationException
668    {
669        for (Map.Entry<String, ModelViewItem> criteriaRole : criteriaRoles.entrySet())
670        {
671            String role = criteriaRole.getKey();
672            ModelViewItem<SearchModelCriterionDefinition> criterionViewItem = criteriaRole.getValue();
673            try
674            {
675                SearchModelCriterionDefinition criterion = _criterionDefinitionManager.lookup(role);
676                criterion.setModel(this);
677                criterionViewItem.setDefinition(criterion);
678                
679                if (checkFacetable && !_isFacetable(criterion))
680                {
681                    throw new ConfigurationException("The criterion definition of id '" + criterion.getName() + "' is not facetable.");
682                }
683            }
684            catch (ComponentException e)
685            {
686                throw new ConfigurationException("Impossible to lookup the criterion definition of role: " + role, e);
687            }
688        }
689    }
690    
691    /**
692     * Copy the given criteria
693     * Groups are ignored and criteria are filtered by the given {@link Predicate}
694     * @param criteria the criteria to copy
695     * @param filter the filter to apply on copied criteria
696     * @return the copied criteria
697     * @throws ConfigurationException if an error occurs.
698     */
699    protected ViewItemContainer _copyCriteria(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException
700    {
701        ViewItemContainer copy = new View();
702        copy.addViewItems(_copyCriteriaViewItems(criteria, filter));
703        
704        return copy;
705    }
706    
707    /**
708     * Retrieves the copies of the given criteria
709     * @param criteria the criteria to copy
710     * @param filter the filter to apply on copied criteria
711     * @return the copied criteria
712     * @throws ConfigurationException if an error occurs.
713     */
714    protected List<ViewItem> _copyCriteriaViewItems(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException
715    {
716        List<ViewItem> viewItems = new ArrayList<>();
717        
718        for (ViewItem viewItem : criteria.getViewItems())
719        {
720            if (viewItem instanceof ViewItemContainer group)
721            {
722                viewItems.addAll(_copyCriteriaViewItems(group, filter));
723            }
724            
725            if (viewItem instanceof SearchModelCriterionViewItem criterionViewItem && filter.test(criterionViewItem.getDefinition()))
726            {
727                ViewItem advancedCriterionViewItem = criterionViewItem.createInstance();
728                criterionViewItem.copyTo(advancedCriterionViewItem);
729                viewItems.add(advancedCriterionViewItem);
730            }
731        }
732        
733        return viewItems;
734    }
735    
736    /**
737     * Test if a criterion definition can be used in advanced search mode.
738     * For instance: geocode, rich-text, file-typed criterion are not allowed.
739     * @param modelItem the criterion definition to test.
740     * @return <code>true</code> if the criterion can be used in advanced search mode, <code>false</code> otherwise.
741     */
742    @SuppressWarnings("static-access")
743    protected boolean _isAdvanced(ModelItem modelItem)
744    {
745        String typeId = modelItem.getType().getId();
746        switch (typeId)
747        {
748            case ModelItemTypeConstants.STRING_TYPE_ID:
749            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
750            case ModelItemTypeConstants.LONG_TYPE_ID:
751            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
752            case ModelItemTypeConstants.DATE_TYPE_ID:
753            case ModelItemTypeConstants.DATETIME_TYPE_ID:
754            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
755            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
756            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
757            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
758                return true;
759            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
760            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
761            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
762            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
763            default:
764                return false;
765        }
766    }
767    
768    /**
769     * Test if a criterion definition can be used as a facet
770     * @param modelItem the criterion definition to test.
771     * @return <code>true</code> if the criterion can be used as a facet, <code>false</code> otherwise.
772     */
773    protected boolean _isFacetable(ModelItem modelItem)
774    {
775        if (modelItem instanceof ReferencingSearchModelCriterionDefinition criterionDefinition)
776        {
777            ElementDefinition reference = criterionDefinition.getReference();
778            return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
779                    ? indexationAwareElementDefinition.isFacetable()
780                    : modelItem instanceof Property
781                        ? false
782                        : criterionDefinition.getType().isFacetable(DataContext.newInstance()
783                                                                               .withModelItem(criterionDefinition));
784        }
785        else
786        {
787            return false;
788        }
789    }
790
791    public String getId()
792    {
793        return _id;
794    }
795    
796    private record SearchModelCriterionViewItemWithRole(ModelViewItem criterionViewItem, Optional<String> role) { /* empty */ }
797}