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            removeDuplicatedCriteria(criteria);
254            setCriteria(criteria);
255            
256            removeDuplicatedCriteria(advancedCriteria);
257            setAdvancedCriteria(advancedCriteria);
258            
259            removeDuplicatedCriteria(facetedCriteria);
260            setFacetedCriteria(facetedCriteria);
261            
262            Configuration columnConfs = searchConfig.getChild("columns").getChild("default");
263            ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes);
264            try
265            {
266                LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null);
267                setResultItems(parser.parseView(new ConfigurationAndPluginName(columnConfs, "")));
268            }
269            catch (Exception e)
270            {
271                throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e);
272            }
273            finally
274            {
275                LifecycleHelper.dispose(parser);
276            }
277        }
278        catch (Exception e)
279        {
280            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
281        }
282    }
283    
284    @Override
285    public void dispose()
286    {
287        _criterionDefinitionManager.dispose();
288        _criterionDefinitionManager = null;
289    }
290    
291    /**
292     * Configure the content type ids
293     * @param configuration The content types configuration
294     * @throws ConfigurationException If an error occurs
295     */
296    protected void _configureContentTypes(Configuration configuration) throws ConfigurationException
297    {
298        Set<String> contentTypes = new HashSet<>();
299        Set<String> excludedContentTypes = new HashSet<>();
300        
301        if (configuration != null)
302        {
303            Configuration excludeConf = configuration.getChild("exclude");
304            
305            List<String> excludedTags = new ArrayList<>();
306            for (Configuration tagCong : excludeConf.getChildren("tag"))
307            {
308                excludedTags.add(tagCong.getValue());
309            }
310            
311            List<String> excludedCTypes = new ArrayList<>();
312            for (Configuration cType : excludeConf.getChildren("content-type"))
313            {
314                excludedCTypes.add(cType.getValue());
315            }
316            
317            Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
318            if (cTypesConfiguration.length == 0)
319            {
320                // Keep "content types" empty.
321                for (String id : _contentTypeExtensionPoint.getExtensionsIds())
322                {
323                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
324                    {
325                        excludedContentTypes.add(id);
326                    }
327                }
328            }
329            else
330            {
331                for (Configuration conf : configuration.getChildren("content-type"))
332                {
333                    String id = conf.getAttribute("id");
334                    contentTypes.add(id);
335                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
336                    {
337                        excludedContentTypes.add(id);
338                    }
339                    
340                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id))
341                    {
342                        if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes))
343                        {
344                            excludedContentTypes.add(subTypeId);
345                        }
346                    }
347                }
348            }
349        }
350        
351        setContentTypes(contentTypes);
352        setExcludedContentTypes(excludedContentTypes);
353    }
354    
355    /**
356     * Configure the base content type ids.
357     * @param configuration The content types configuration
358     * @return The set of base content type ids
359     * @throws ConfigurationException If an error occurs
360     */
361    protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
362    {
363        Set<String> cTypes = new HashSet<>();
364        
365        Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
366        if (cTypesConfiguration.length == 0)
367        {
368            cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds());
369        }
370        else
371        {
372            for (Configuration conf : cTypesConfiguration)
373            {
374                cTypes.add(conf.getAttribute("id"));
375            }
376        }
377        
378        return cTypes;
379    }
380    
381    /**
382     * Determines if the content type is a valid content type in current configuration 
383     * @param id The content type id
384     * @param excludedTags The tags to exclude
385     * @param excludedContentTypes The content types to exclude
386     * @return <code>true</code> if the content type is a valid content type
387     */
388    protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes)
389    {
390        if (excludedContentTypes.contains(id))
391        {
392            return false;
393        }
394        
395        ContentType cType = _contentTypeExtensionPoint.getExtension(id);
396        for (String tag : excludedTags)
397        {
398            if (cType.hasTag(tag))
399            {
400                return false;
401            }
402        }
403        
404        return true;
405    }
406    
407    private void _configureSearchUrl(Configuration configuration)
408    {
409        setSearchUrlPlugin(configuration.getChild("search-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
410        setSearchUrl(configuration.getChild("search-url").getValue(__DEFAULT_SEARCH_URL));
411    }
412    
413    private void _configureExportCSVUrl(Configuration configuration)
414    {
415        setExportCSVUrlPlugin(configuration.getChild("export-csv-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
416        setExportCSVUrl(configuration.getChild("export-csv-url").getValue(__DEFAULT_EXPORT_CSV_URL));
417    }
418    
419    private void _configureExportDOCUrl(Configuration configuration)
420    {
421        setExportDOCUrlPlugin(configuration.getChild("export-doc-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
422        setExportDOCUrl(configuration.getChild("export-doc-url").getValue(__DEFAULT_EXPORT_DOC_URL));
423    }
424
425    private void _configureExportXMLUrl(Configuration configuration)
426    {
427        setExportXMLUrlPlugin(configuration.getChild("export-xml-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
428        setExportXMLUrl(configuration.getChild("export-xml-url").getValue(__DEFAULT_EXPORT_XML_URL));
429    }
430
431    private void _configureExportPDFUrl(Configuration configuration)
432    {
433        setExportPDFUrlPlugin(configuration.getChild("export-pdf-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
434        setExportPDFUrl(configuration.getChild("export-pdf-url").getValue(__DEFAULT_EXPORT_PDF_URL));
435    }
436    
437    private void _configurePrintUrl(Configuration configuration)
438    {
439        setPrintUrlPlugin(configuration.getChild("print-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
440        setPrintUrl(configuration.getChild("print-url").getValue(__DEFAULT_PRINT_URL));
441    }
442    
443    /**
444     * Parses criteria in the given configuration.
445     * Add components in Search criteria manager and fill the Map with roles and {@link SearchModelCriterionViewItem}
446     * @param contentTypes the model's content types
447     * @param configuration the model's configuration.
448     * @param rolesByUICriteria the map to fill with the {@link SearchModelCriterionViewItem} and the role of the referenced {@link SearchModelCriterionDefinition}
449     * @param acceptGroups <code>true</code> if the parsed criteria is able to have groups, <code>false</code> otherwise
450     * @return the parsed criteria
451     * @throws ConfigurationException if an error occurs.
452     */
453    protected ViewItemContainer _parseCriteria(Set<ContentType> contentTypes, Configuration configuration, Map<String, ModelViewItem> rolesByUICriteria, boolean acceptGroups) throws ConfigurationException
454    {
455        ViewItemContainer criteria = new View();
456        ViewItemAccessor criteriaWithoutGroupAccessor = criteria;
457        
458        if (acceptGroups)
459        {
460            for (Configuration groupConf : configuration.getChildren("group"))
461            {
462                SimpleViewItemGroup group = new SimpleViewItemGroup();
463                group.setRole(groupConf.getAttribute("role", ViewItemGroup.FIELDSET_ROLE));
464                group.setName(groupConf.getAttribute("name", null));
465                
466                ConfigurationAndPluginName groupConfAndPluginName = new ConfigurationAndPluginName(groupConf, _pluginName);
467                group.setLabel(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "label"));
468                group.setDescription(ItemParserHelper.parseI18nizableText(groupConfAndPluginName, "description"));
469                
470                criteria.addViewItem(group);
471                
472                List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, groupConf);
473                group.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList());
474                uiCriteriaWithRoles.stream()
475                                   .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent())
476                                   .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem()));
477            }
478            
479            // Group for criteria with no group
480            SimpleViewItemGroup group = new SimpleViewItemGroup();
481            group.setRole(ViewItemGroup.FIELDSET_ROLE);
482            criteria.addViewItem(group);
483            criteriaWithoutGroupAccessor = group;
484        }
485        
486        // Criteria with no group
487        List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = _parseCriteria(contentTypes, configuration);
488        criteriaWithoutGroupAccessor.addViewItems(uiCriteriaWithRoles.stream().map(SearchModelCriterionViewItemWithRole::criterionViewItem).toList());
489        uiCriteriaWithRoles.stream()
490                           .filter(criterionViewItemWithRole -> criterionViewItemWithRole.role().isPresent())
491                           .forEachOrdered(criterionViewItemWithRole -> rolesByUICriteria.put(criterionViewItemWithRole.role().get(), criterionViewItemWithRole.criterionViewItem()));
492        
493        return criteria;
494    }
495    
496    /**
497     * Parses criteria in the given configuration.
498     * Add components in Search criteria manager
499     * @param contentTypes the model's content types
500     * @param configuration the model's configuration.
501     * @return the Map with roles and {@link SearchModelCriterionViewItem}
502     * @throws ConfigurationException if an error occurs.
503     */
504    protected List<SearchModelCriterionViewItemWithRole> _parseCriteria(Set<ContentType> contentTypes, Configuration configuration) throws ConfigurationException
505    {
506        List<SearchModelCriterionViewItemWithRole> uiCriteriaWithRoles = new ArrayList<>();
507        
508        for (Configuration conf : configuration.getChildren("item"))
509        {
510            String reference = conf.getAttribute("ref", null);
511            
512            if (StringUtils.isNotEmpty(reference))
513            {
514                uiCriteriaWithRoles.addAll(_parseReferencingCriteria(contentTypes, conf, reference));
515            }
516            else
517            {
518                uiCriteriaWithRoles.addAll(_parseStaticCriteria(contentTypes, conf));
519            }
520        }
521        
522        return uiCriteriaWithRoles;
523    }
524    
525    /**
526     * Parses a criteria that references a model item of model's content types, or a system property
527     * Add a referencing criteria component to the manager.
528     * @param contentTypes the model's content types.
529     * @param conf the criteria configuration.
530     * @param reference the path of the referenced item
531     * @return A Map with the created UI criteria and potential role of the corresponding criterion
532     * @throws ConfigurationException if an error occurs.
533     */
534    protected List<SearchModelCriterionViewItemWithRole> _parseReferencingCriteria(Set<ContentType> contentTypes, Configuration conf, String reference) throws ConfigurationException
535    {
536        try
537        {
538            if (ViewParser.ALL_ITEMS_REFERENCE.equals(reference))
539            {
540                List<SearchModelCriterionViewItemWithRole> viewItemsWithRoles = new ArrayList<>();
541
542                if (contentTypes.isEmpty())
543                {
544                    // There is no given content types, only add the title attribute
545                    SearchModelCriterionViewItem titleCriterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, _contentTypesHelper.getTitleAttributeDefinition(), Content.ATTRIBUTE_TITLE);
546                    viewItemsWithRoles.add(new SearchModelCriterionViewItemWithRole(titleCriterionViewItem, Optional.empty()));
547                }
548                else
549                {
550                    // Add criteria for all model items of the given content types
551                    for (ContentType contentType : contentTypes)
552                    {
553                        for (ModelItem modelItem : contentType.getModelItems())
554                        {
555                            _getCriterionViewItemWithRole(modelItem).ifPresent(viewItemsWithRoles::add);
556                        }
557                    }
558                }
559                
560                // Add criteria for all system properties
561                for (String systemPropertyId : _systemPropertyExtensionPoint.getExtensionsIds())
562                {
563                    SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(systemPropertyId);
564                    _getCriterionViewItemWithRole(systemProperty).ifPresent(viewItemsWithRoles::add);
565                }
566                
567                return viewItemsWithRoles;
568            }
569            else
570            {
571                if (contentTypes.isEmpty() && !Content.ATTRIBUTE_TITLE.equals(reference) && !_systemPropertyExtensionPoint.hasExtension(reference))
572                {
573                    throw new ConfigurationException("The criteria '" + reference + "' is forbidden when no content type is specified: only title or system properties can be used.");
574                }
575                
576                // The criteria references a specific item
577                String role = reference + _criteriaIndex;
578                _criteriaIndex++;
579                SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf);
580                
581                Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes);
582                Class<? extends SearchModelCriterionDefinition> criterionDefinitionClass = _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference);
583                if (criterionDefinitionClass != null)
584                {
585                    _criterionDefinitionManager.addComponent("cms", null, role, _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, reference), wrapConf);
586                    return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role)));
587                }
588                else
589                {
590                    return List.of();
591                }
592            }
593        }
594        catch (Exception e)
595        {
596            throw new ConfigurationException("Unable to instanciate criterion definition referencing " + reference, conf, e);
597        }
598    }
599    
600    private Optional<SearchModelCriterionViewItemWithRole> _getCriterionViewItemWithRole(ModelItem modelItem)
601    {
602        if (modelItem instanceof ElementDefinition definition // Get only first-level field (ignore composites and repeaters)
603                && (!(definition instanceof Property) || definition instanceof CriterionDefinitionAwareElementDefinition)) // Exclude properties that are not criterion aware
604        {
605            SearchModelCriterionViewItem criterionViewItem = _searchUIModelCriterionDefinitionHelper.createReferencingCriterionViewItem(this, definition, definition.getName());
606            if (criterionViewItem != null)
607            {
608                return Optional.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.empty()));
609            }
610        }
611        
612        return Optional.empty();
613    }
614    
615    /**
616     * Parses a static criteria
617     * Add a referencing criteria component to the manager.
618     * @param contentTypes the model's content types.
619     * @param conf the criteria configuration.
620     * @return A Map with the created UI criteria and potential role of the corresponding criterion
621     * @throws ConfigurationException if an error occurs.
622     */
623    protected List<SearchModelCriterionViewItemWithRole> _parseStaticCriteria(Set<ContentType> contentTypes, Configuration conf) throws ConfigurationException
624    {
625        String criteriaName = conf.getAttribute("name", null);
626        String className = conf.getAttribute("class", null);
627        
628        if (criteriaName == null || className == null)
629        {
630            throw new ConfigurationException("The custom criterion definition '" + criteriaName + "' does not specifiy a class or a name.", conf);
631        }
632        
633        try
634        {
635            String role = criteriaName + _criteriaIndex;
636            _criteriaIndex++;
637            
638            SearchModelCriterionViewItem criterionViewItem = _parseCriterionViewItem(conf);
639            
640            @SuppressWarnings("unchecked")
641            Class<SearchModelCriterionDefinition> criterionDefinitionClass = (Class<SearchModelCriterionDefinition>) Class.forName(className);
642            Configuration wrapConf = _criterionDefinitionHelper.wrapCriterionConfiguration(conf, contentTypes);
643            _criterionDefinitionManager.addComponent("cms", null, role, criterionDefinitionClass, wrapConf);
644            
645            return List.of(new SearchModelCriterionViewItemWithRole(criterionViewItem, Optional.of(role)));
646        }
647        catch (Exception e)
648        {
649            throw new ConfigurationException("Unable to instanciate custom criterion view item for class: " + className, conf, e);
650        }
651    }
652    
653    /**
654     * Parses the {@link SearchModelCriterionViewItem} from the given configuration
655     * @param configuration the configuration
656     * @return the parsed UI criterion
657     * @throws ConfigurationException if an error occurs
658     */
659    protected SearchModelCriterionViewItem _parseCriterionViewItem(Configuration configuration) throws ConfigurationException
660    {
661        SearchModelCriterionViewItem criterionViewItem = new DefaultSearchModelCriterionViewItem();
662        criterionViewItem.setHidden(configuration.getAttributeAsBoolean("hidden", false));
663        return criterionViewItem;
664    }
665    
666    /**
667     * Lookup the previously initialized criteria components and add the criteria to their pending {@link SearchModelCriterionViewItem}
668     * @param criteriaRoles the criteria map to fill.
669     * @param checkFacetable true to check if the criteria are facetable.
670     * @throws ConfigurationException if an error occurs.
671     */
672    protected void _lookupCriteriaComponents(Map<String, ModelViewItem> criteriaRoles, boolean checkFacetable) throws ConfigurationException
673    {
674        for (Map.Entry<String, ModelViewItem> criteriaRole : criteriaRoles.entrySet())
675        {
676            String role = criteriaRole.getKey();
677            ModelViewItem<SearchModelCriterionDefinition> criterionViewItem = criteriaRole.getValue();
678            try
679            {
680                SearchModelCriterionDefinition criterion = _criterionDefinitionManager.lookup(role);
681                criterion.setModel(this);
682                criterionViewItem.setDefinition(criterion);
683                
684                if (checkFacetable && !_isFacetable(criterion))
685                {
686                    throw new ConfigurationException("The criterion definition of id '" + criterion.getName() + "' is not facetable.");
687                }
688            }
689            catch (ComponentException e)
690            {
691                throw new ConfigurationException("Impossible to lookup the criterion definition of role: " + role, e);
692            }
693        }
694    }
695    
696    /**
697     * Copy the given criteria
698     * Groups are ignored and criteria are filtered by the given {@link Predicate}
699     * @param criteria the criteria to copy
700     * @param filter the filter to apply on copied criteria
701     * @return the copied criteria
702     * @throws ConfigurationException if an error occurs.
703     */
704    protected ViewItemContainer _copyCriteria(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException
705    {
706        ViewItemContainer copy = new View();
707        copy.addViewItems(_copyCriteriaViewItems(criteria, filter));
708        
709        return copy;
710    }
711    
712    /**
713     * Retrieves the copies of the given criteria
714     * @param criteria the criteria to copy
715     * @param filter the filter to apply on copied criteria
716     * @return the copied criteria
717     * @throws ConfigurationException if an error occurs.
718     */
719    protected List<ViewItem> _copyCriteriaViewItems(ViewItemContainer criteria, Predicate<ModelItem> filter) throws ConfigurationException
720    {
721        List<ViewItem> viewItems = new ArrayList<>();
722        
723        for (ViewItem viewItem : criteria.getViewItems())
724        {
725            if (viewItem instanceof ViewItemContainer group)
726            {
727                viewItems.addAll(_copyCriteriaViewItems(group, filter));
728            }
729            
730            if (viewItem instanceof SearchModelCriterionViewItem criterionViewItem && filter.test(criterionViewItem.getDefinition()))
731            {
732                ViewItem advancedCriterionViewItem = criterionViewItem.createInstance();
733                criterionViewItem.copyTo(advancedCriterionViewItem);
734                viewItems.add(advancedCriterionViewItem);
735            }
736        }
737        
738        return viewItems;
739    }
740    
741    /**
742     * Test if a criterion definition can be used in advanced search mode.
743     * For instance: geocode, rich-text, file-typed criterion are not allowed.
744     * @param modelItem the criterion definition to test.
745     * @return <code>true</code> if the criterion can be used in advanced search mode, <code>false</code> otherwise.
746     */
747    @SuppressWarnings("static-access")
748    protected boolean _isAdvanced(ModelItem modelItem)
749    {
750        String typeId = modelItem.getType().getId();
751        switch (typeId)
752        {
753            case ModelItemTypeConstants.STRING_TYPE_ID:
754            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
755            case ModelItemTypeConstants.LONG_TYPE_ID:
756            case ModelItemTypeConstants.DOUBLE_TYPE_ID:
757            case ModelItemTypeConstants.DATE_TYPE_ID:
758            case ModelItemTypeConstants.DATETIME_TYPE_ID:
759            case ModelItemTypeConstants.BOOLEAN_TYPE_ID:
760            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
761            case ModelItemTypeConstants.USER_ELEMENT_TYPE_ID:
762            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
763                return true;
764            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
765            case ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID:
766            case ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID:
767            case ModelItemTypeConstants.GEOCODE_ELEMENT_TYPE_ID:
768            default:
769                return false;
770        }
771    }
772    
773    /**
774     * Test if a criterion definition can be used as a facet
775     * @param modelItem the criterion definition to test.
776     * @return <code>true</code> if the criterion can be used as a facet, <code>false</code> otherwise.
777     */
778    protected boolean _isFacetable(ModelItem modelItem)
779    {
780        if (modelItem instanceof ReferencingSearchModelCriterionDefinition criterionDefinition)
781        {
782            ElementDefinition reference = criterionDefinition.getReference();
783            return reference instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
784                    ? indexationAwareElementDefinition.isFacetable()
785                    : modelItem instanceof Property
786                        ? false
787                        : criterionDefinition.getType().isFacetable(DataContext.newInstance()
788                                                                               .withModelItem(criterionDefinition));
789        }
790        else
791        {
792            return false;
793        }
794    }
795    
796    /**
797     * Deduplicates criteria by definition name in the given container container. It keeps the last occurrence when duplicates exist.
798     * @param criteria the criteria to deduplicate
799     */
800    protected void removeDuplicatedCriteria(ViewItemContainer criteria)
801    {
802        Map<String, ViewItem> existingDefinitions = new HashMap<>();
803        Set<ViewItem> criteriaToRemove = new HashSet<>();
804        for (ViewItem viewItem : criteria.getViewItems())
805        {
806            if (viewItem instanceof SearchModelCriterionViewItem criterionViewItem)
807            {
808                String definitionName = Optional.ofNullable(criterionViewItem.getDefinition())
809                                                .map(ModelItem::getName)
810                                                .orElse(null);
811                
812                if (existingDefinitions.containsKey(definitionName))
813                {
814                    criteriaToRemove.add(existingDefinitions.get(definitionName));
815                }
816
817                existingDefinitions.put(definitionName, viewItem);
818            }
819        }
820        
821        for (ViewItem viewItem : criteriaToRemove)
822        {
823            criteria.removeViewItem(viewItem);
824        }
825    }
826
827    public String getId()
828    {
829        return _id;
830    }
831    
832    private record SearchModelCriterionViewItemWithRole(ModelViewItem criterionViewItem, Optional<String> role) { /* empty */ }
833}