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.i18n.I18nizableText;
061import org.ametys.runtime.model.ElementDefinition;
062import org.ametys.runtime.model.ItemParserHelper;
063import org.ametys.runtime.model.ItemParserHelper.ConfigurationAndPluginName;
064import org.ametys.runtime.model.ModelItem;
065import org.ametys.runtime.model.ModelViewItem;
066import org.ametys.runtime.model.SimpleViewItemGroup;
067import org.ametys.runtime.model.View;
068import org.ametys.runtime.model.ViewItem;
069import org.ametys.runtime.model.ViewItemAccessor;
070import org.ametys.runtime.model.ViewItemContainer;
071import org.ametys.runtime.model.ViewItemGroup;
072import org.ametys.runtime.model.ViewParser;
073import org.ametys.runtime.model.ViewParserContext;
074import org.ametys.runtime.model.type.DataContext;
075import org.ametys.runtime.plugin.component.LogEnabled;
076import org.ametys.runtime.plugin.component.PluginAware;
077import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
078
079/**
080 * Static implementation of a {@link SearchUIModel}
081 */
082public class StaticSearchUIModel extends DefaultSearchUIModel implements Serviceable, Contextualizable, Configurable, Disposable, LogEnabled, PluginAware
083{
084    /** ComponentManager for {@link SearchModelCriterionDefinition}s. */
085    protected ThreadSafeComponentManager<SearchModelCriterionDefinition> _criterionDefinitionManager;
086    
087    /** The context. */
088    protected Context _context;
089    
090    /** The service manager */
091    protected ServiceManager _manager;
092    
093    /** The plugin name */
094    protected String _pluginName;
095    
096    /** The content type helper. */
097    protected ContentTypesHelper _contentTypesHelper;
098    
099    /** The helper for columns */
100    protected ColumnHelper _columnHelper;
101    
102    /** The content type extension point */
103    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
104    
105    /** The system property extension point. */
106    protected SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
107    
108    /** The criterion definition helper */
109    protected CriterionDefinitionHelper _criterionDefinitionHelper;
110    
111    /** The helper for search model criterion definition */
112    protected SearchModelCriterionDefinitionHelper _searchModelCriterionDefinitionHelper;
113    
114    /** The helper for search model criterion definition */
115    protected SearchModelCriterionViewItemHelper _searchUIModelCriterionDefinitionHelper;
116    
117    /** The logger. */
118    protected Logger _logger;
119    
120    /** The search model identifier */
121    protected String _id;
122    
123    private int _criteriaIndex;
124
125    public void setLogger(final Logger logger)
126    {
127        _logger = logger;
128    }
129    
130    /**
131     * Get the logger.
132     * @return the logger.
133     */
134    protected final Logger getLogger()
135    {
136        return _logger;
137    }
138    
139    @Override
140    public void contextualize(Context context) throws ContextException
141    {
142        _context = context;
143    }
144    
145    public void service(ServiceManager manager) throws ServiceException
146    {
147        _manager = manager;
148        
149        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
150        _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE);
151        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
152        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
153        _criterionDefinitionHelper = (CriterionDefinitionHelper) manager.lookup(CriterionDefinitionHelper.ROLE);
154        _searchModelCriterionDefinitionHelper = (SearchModelCriterionDefinitionHelper) manager.lookup(SearchModelCriterionDefinitionHelper.ROLE);
155        _searchUIModelCriterionDefinitionHelper = (SearchModelCriterionViewItemHelper) manager.lookup(SearchModelCriterionViewItemHelper.ROLE);
156    }
157
158    public void setPluginInfo(String pluginName, String featureName, String id)
159    {
160        _pluginName = pluginName;
161        _id = id;
162    }
163    
164    @Override
165    public void configure(Configuration configuration) throws ConfigurationException
166    {
167        try
168        {
169            _criterionDefinitionManager = new ThreadSafeComponentManager<>();
170            _criterionDefinitionManager.setLogger(getLogger());
171            _criterionDefinitionManager.contextualize(_context);
172            _criterionDefinitionManager.service(_manager);
173            
174            Configuration searchConfig = configuration.getChild("SearchModel");
175            setLabel(I18nizableText.parseI18nizableText(searchConfig.getChild("label", false), "plugin." + _pluginName));
176            setDescription(I18nizableText.parseI18nizableText(searchConfig.getChild("description", false), "plugin." + _pluginName));
177            
178            _configureTags(searchConfig.getChild("tags"));
179            
180            _configureContentTypes(searchConfig.getChild("content-types", false));
181            
182            setSummaryView(searchConfig.getChild("summary-view").getValue(null));
183            setAllowSortOnMultipleJoin(searchConfig.getChild("allow-sort-on-multiple-join").getValueAsBoolean(super.allowSortOnMultipleJoin()));
184            
185            // Get the base content types and try to find common ancestors.
186            Set<String> baseCTypeIds = _configureBaseContentTypes(searchConfig.getChild("content-types"));
187            Set<ContentType> commonContentTypes = _contentTypesHelper.getCommonAncestors(baseCTypeIds)
188                    .stream()
189                    .map(_contentTypeExtensionPoint::getExtension)
190                    .collect(Collectors.toSet());
191            
192            int pageSize = searchConfig.getChild("page-size").getValueAsInteger(50);
193            if (pageSize < 0)
194            {
195                pageSize = 50;
196            }
197            setPageSize(pageSize);
198            setWorkspace(searchConfig.getChild("workspace").getValue(null));
199            
200            _configureSearchUrl(searchConfig);
201            _configureExportCSVUrl(searchConfig);
202            _configurePrintUrl(searchConfig);
203            
204            _criteriaIndex = 0;
205            Map<String, ModelViewItem> criteriaRoles = new HashMap<>();
206            Map<String, ModelViewItem> advancedCriteriaRoles = new HashMap<>();
207            Map<String, ModelViewItem> facetedCriteriaRoles = new HashMap<>();
208            
209            Configuration criteriaConf = searchConfig.getChild("simple-search-criteria");
210            ViewItemContainer criteria = _parseCriteria(commonContentTypes, criteriaConf, criteriaRoles, true);
211            
212            Configuration advancedCriteriaConf = searchConfig.getChild("advanced-search-criteria", false);
213            ViewItemContainer advancedCriteria = new View();
214            if (advancedCriteriaConf != null)
215            {
216                advancedCriteria = _parseCriteria(commonContentTypes, advancedCriteriaConf, advancedCriteriaRoles, false);
217            }
218            
219            Configuration facetsConf = searchConfig.getChild("facets", false);
220            ViewItemContainer facetedCriteria = new View();
221            if (facetsConf != null)
222            {
223                facetedCriteria = _parseCriteria(commonContentTypes, facetsConf, facetedCriteriaRoles, false);
224            }
225            
226            _criterionDefinitionManager.initialize();
227            
228            _lookupCriteriaComponents(criteriaRoles, false);
229            if (advancedCriteriaConf != null)
230            {
231                if (advancedCriteria.getViewItems().isEmpty())
232                {
233                    // The advanced criteria root configuration is present but has no criteria:
234                    // copy the simple criteria.
235                    advancedCriteria = _copyCriteria(criteria, this::_isAdvanced);
236                }
237                else
238                {
239                    _lookupCriteriaComponents(advancedCriteriaRoles, false);
240                }
241            }
242            
243            if (facetsConf != null)
244            {
245                if (facetedCriteria.getViewItems().isEmpty())
246                {
247                    // The facet root configuration is present but has no criteria:
248                    // copy the simple criteria which are facetable.
249                    facetedCriteria = _copyCriteria(criteria, this::_isFacetable);
250                }
251                else
252                {
253                    _lookupCriteriaComponents(facetedCriteriaRoles, true);
254                }
255            }
256            
257            setCriteria(criteria);
258            setAdvancedCriteria(advancedCriteria);
259            setFacetedCriteria(facetedCriteria);
260            
261            Configuration columnConfs = searchConfig.getChild("columns").getChild("default");
262            ViewParser parser = new StaticSearchUIModelColumnsParser(commonContentTypes);
263            try
264            {
265                LifecycleHelper.setupComponent(parser, new SLF4JLoggerAdapter(getLogger()), _context, _manager, null);
266                setResultItems(parser.parseView(new ConfigurationAndPluginName(columnConfs, ""), ViewParserContext.newInstance()));
267            }
268            catch (Exception e)
269            {
270                throw new ConfigurationException("Unable to parse columns of search model", columnConfs, e);
271            }
272            finally
273            {
274                LifecycleHelper.dispose(parser);
275            }
276        }
277        catch (Exception e)
278        {
279            throw new ConfigurationException("Unable to create local component managers.", configuration, e);
280        }
281    }
282    
283    private void _configureTags(Configuration tagsConfiguration)
284    {
285        Set<String> tags = new HashSet<>();
286        for (Configuration tagCfg : tagsConfiguration.getChildren("tag"))
287        {
288            String tag = tagCfg.getValue(null);
289            if (StringUtils.isNotBlank(tag))
290            {
291                tags.add(tag);
292            }
293        }
294        setTags(tags);
295    }
296
297    @Override
298    public void dispose()
299    {
300        _criterionDefinitionManager.dispose();
301        _criterionDefinitionManager = null;
302    }
303    
304    /**
305     * Configure the content type ids
306     * @param configuration The content types configuration
307     * @throws ConfigurationException If an error occurs
308     */
309    protected void _configureContentTypes(Configuration configuration) throws ConfigurationException
310    {
311        Set<String> contentTypes = new HashSet<>();
312        Set<String> excludedContentTypes = new HashSet<>();
313        
314        if (configuration != null)
315        {
316            Configuration excludeConf = configuration.getChild("exclude");
317            
318            List<String> excludedTags = new ArrayList<>();
319            for (Configuration tagCong : excludeConf.getChildren("tag"))
320            {
321                excludedTags.add(tagCong.getValue());
322            }
323            
324            List<String> excludedCTypes = new ArrayList<>();
325            for (Configuration cType : excludeConf.getChildren("content-type"))
326            {
327                excludedCTypes.add(cType.getValue());
328            }
329            
330            Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
331            if (cTypesConfiguration.length == 0)
332            {
333                // Keep "content types" empty.
334                for (String id : _contentTypeExtensionPoint.getExtensionsIds())
335                {
336                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
337                    {
338                        excludedContentTypes.add(id);
339                    }
340                }
341            }
342            else
343            {
344                for (Configuration conf : configuration.getChildren("content-type"))
345                {
346                    String id = conf.getAttribute("id");
347                    contentTypes.add(id);
348                    if (!_isValidContentType(id, excludedTags, excludedCTypes))
349                    {
350                        excludedContentTypes.add(id);
351                    }
352                    
353                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(id))
354                    {
355                        if (!_isValidContentType(subTypeId, excludedTags, excludedCTypes))
356                        {
357                            excludedContentTypes.add(subTypeId);
358                        }
359                    }
360                }
361            }
362        }
363        
364        setContentTypes(contentTypes);
365        setExcludedContentTypes(excludedContentTypes);
366    }
367    
368    /**
369     * Configure the base content type ids.
370     * @param configuration The content types configuration
371     * @return The set of base content type ids
372     * @throws ConfigurationException If an error occurs
373     */
374    protected Set<String> _configureBaseContentTypes(Configuration configuration) throws ConfigurationException
375    {
376        Set<String> cTypes = new HashSet<>();
377        
378        Configuration[] cTypesConfiguration = configuration.getChildren("content-type");
379        if (cTypesConfiguration.length == 0)
380        {
381            cTypes.addAll(_contentTypeExtensionPoint.getExtensionsIds());
382        }
383        else
384        {
385            for (Configuration conf : cTypesConfiguration)
386            {
387                cTypes.add(conf.getAttribute("id"));
388            }
389        }
390        
391        return cTypes;
392    }
393    
394    /**
395     * Determines if the content type is a valid content type in current configuration
396     * @param id The content type id
397     * @param excludedTags The tags to exclude
398     * @param excludedContentTypes The content types to exclude
399     * @return <code>true</code> if the content type is a valid content type
400     */
401    protected boolean _isValidContentType (String id, List<String> excludedTags, List<String> excludedContentTypes)
402    {
403        if (excludedContentTypes.contains(id))
404        {
405            return false;
406        }
407        
408        ContentType cType = _contentTypeExtensionPoint.getExtension(id);
409        for (String tag : excludedTags)
410        {
411            if (cType.hasTag(tag))
412            {
413                return false;
414            }
415        }
416        
417        return true;
418    }
419    
420    private void _configureSearchUrl(Configuration configuration)
421    {
422        setSearchUrlPlugin(configuration.getChild("search-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
423        setSearchUrl(configuration.getChild("search-url").getValue(__DEFAULT_SEARCH_URL));
424    }
425    
426    private void _configureExportCSVUrl(Configuration configuration)
427    {
428        setExportCSVUrlPlugin(configuration.getChild("export-csv-url").getAttribute("plugin", __DEFAULT_URL_PLUGIN));
429        setExportCSVUrl(configuration.getChild("export-csv-url").getValue(__DEFAULT_EXPORT_CSV_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(_pluginName, 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(_pluginName, 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}