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.impl;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.configuration.Configuration;
028import org.apache.avalon.framework.configuration.ConfigurationException;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.commons.lang3.BooleanUtils;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.content.properties.AbstractMultiValuesProperty;
035import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.contenttype.MetadataType;
039import org.ametys.cms.data.type.ModelItemTypeConstants;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.search.QueryBuilder;
042import org.ametys.cms.search.SearchField;
043import org.ametys.cms.search.content.ContentSearchHelper;
044import org.ametys.cms.search.model.IndexingFieldSearchCriterion;
045import org.ametys.cms.search.query.AndQuery;
046import org.ametys.cms.search.query.BooleanQuery;
047import org.ametys.cms.search.query.ContentQuery;
048import org.ametys.cms.search.query.DateQuery;
049import org.ametys.cms.search.query.DoubleQuery;
050import org.ametys.cms.search.query.GeocodeQuery;
051import org.ametys.cms.search.query.JoinQuery;
052import org.ametys.cms.search.query.LongQuery;
053import org.ametys.cms.search.query.OrQuery;
054import org.ametys.cms.search.query.Query;
055import org.ametys.cms.search.query.Query.Operator;
056import org.ametys.cms.search.query.RichTextQuery;
057import org.ametys.cms.search.query.StringQuery;
058import org.ametys.cms.search.query.UsersQuery;
059import org.ametys.core.user.UserIdentity;
060import org.ametys.core.util.date.AdaptableDateParser;
061import org.ametys.plugins.core.user.UserHelper;
062import org.ametys.plugins.repository.model.RepeaterDefinition;
063import org.ametys.runtime.model.ElementDefinition;
064import org.ametys.runtime.model.ModelHelper;
065import org.ametys.runtime.model.ModelItem;
066import org.ametys.runtime.model.ModelItemGroup;
067import org.ametys.runtime.model.exception.UndefinedItemPathException;
068import org.ametys.runtime.parameter.Validator;
069import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
070
071/**
072 * This class is a search criteria on a metadata of a content
073 */
074public class IndexingFieldSearchUICriterion extends AbstractSearchUICriterion implements IndexingFieldSearchCriterion
075{
076    /** Prefix for id of metadata search criteria */
077    public static final String SEARCH_CRITERIA_METADATA_PREFIX = "metadata-";
078    
079    /** The content search helper. */
080    protected ContentSearchHelper _searchHelper;
081    /** The hierarchical reference tables helper. */
082    protected HierarchicalReferenceTablesHelper _hierarchicalReferenceTablesHelper;
083    /** The user helper */
084    protected UserHelper _userHelper;
085    /** The helper for convenient methods on content types */
086    protected ContentTypesHelper _contentTypesHelper;
087    
088    /** The criteria operator */
089    protected Operator _operator;
090    
091    /** The definition used for this criterion */
092    protected ElementDefinition _definition;
093    
094    /** The field full path */
095    protected String _fullPath;
096    
097    /** The field path */
098    protected String _fieldPath;
099    
100    /** The join paths */
101    protected List<String> _joinPaths;
102    
103    /** Is it AND or OR for multiple metadata */
104    protected boolean _isMultipleOperandAnd;
105    
106    /** ComponentManager for {@link Validator}s. */
107    protected ThreadSafeComponentManager<Validator> _validatorManager;
108    
109    private boolean _isTypeContentWithMultilingualTitle;
110
111    @Override
112    public void service(ServiceManager manager) throws ServiceException
113    {
114        super.service(manager);
115        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
116        _hierarchicalReferenceTablesHelper = (HierarchicalReferenceTablesHelper) manager.lookup(HierarchicalReferenceTablesHelper.ROLE);
117        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
118        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
119    }
120    
121    @Override
122    public void dispose()
123    {
124        super.dispose();
125        
126        _validatorManager.dispose();
127        _validatorManager = null;
128    }
129    
130    @Override
131    public void configure(Configuration configuration) throws ConfigurationException
132    {
133        try
134        {
135            _validatorManager = new ThreadSafeComponentManager<>();
136            _validatorManager.setLogger(_logger);
137            _validatorManager.contextualize(_context);
138            _validatorManager.service(_manager);
139            
140            _enumeratorManager = new ThreadSafeComponentManager<>();
141            _enumeratorManager.setLogger(_logger);
142            _enumeratorManager.contextualize(_context);
143            _enumeratorManager.service(_manager);
144            
145            _fullPath = configuration.getChild("field").getAttribute("path");
146            Set<String> baseContentTypeIds = new HashSet<>();
147            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("baseType"))
148            {
149                baseContentTypeIds.add(cTypeConf.getAttribute("id"));
150            }
151            
152            _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq"));
153            _isMultipleOperandAnd = !StringUtils.equalsIgnoreCase(configuration.getChild("multiple-operand").getValue("or"), "or");
154            _joinPaths = new ArrayList<>();
155            
156            String id = SEARCH_CRITERIA_METADATA_PREFIX + _fullPath + "-" + _operator.getName();
157            
158            if (!baseContentTypeIds.isEmpty())
159            {
160                try
161                {
162                    Collection<ContentType> baseContentTypes = baseContentTypeIds.stream()
163                            .map(_cTypeEP::getExtension)
164                            .collect(Collectors.toList());
165                    ModelItem modelItem = ModelHelper.getModelItem(_fullPath, baseContentTypes);
166                    if (modelItem instanceof ElementDefinition def)
167                    {
168                        _definition = def;
169                    }
170                    else
171                    {
172                        throw new ConfigurationException("Indexing field search criteria with path '" + _fullPath + "' refers to a group");
173                    }
174
175                    _joinPaths = _searchHelper.computeJoinPaths(_fullPath, baseContentTypeIds, false);
176                }
177                catch (UndefinedItemPathException e)
178                {
179                    throw new ConfigurationException("Indexing field search criteria with path '" + _fullPath + "' refers to an unknown model item", e);
180                }
181            }
182            else if (Content.ATTRIBUTE_TITLE.equals(_fullPath))
183            {
184                // Only the title indexing field is allowed if there is no base content types.
185                _definition = _contentTypesHelper.getTitleAttributeDefinition();
186            }
187            else
188            {
189                throw new ConfigurationException("The indexing field '" + _fullPath + "' is forbidden when no content type is specified: only title can be used.");
190            }
191            
192            _fieldPath = _computeFieldPath(_definition);
193            
194            setId(id);
195            setGroup(_configureI18nizableText(configuration.getChild("group", false), null));
196            _configureLabel(configuration, _definition);
197            _configureDescription(configuration, _definition);
198            
199            MetadataType type = MetadataType.fromModelItemType(_definition.getType());
200            setType(type);
201            
202//            setValidator(finalDefinition.getValidator());
203            String validatorRole = "validator";
204            if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration))
205            {
206                validatorRole = null;
207            }
208            
209            setEnumerator(configureEnumerator(configuration, _definition));
210            setWidget(configureWidget(configuration, _definition));
211            setWidgetParameters(configureWidgetParameters(configuration, _definition));
212
213            setMultiple(_definition.isMultiple());
214            
215            _configureTypeContentWithMultilingualTitle(_definition);
216            
217            _configureOperator(configuration, _definition);
218            
219            configureUIProperties(configuration);
220            configureValues(configuration);
221        }
222        catch (Exception e)
223        {
224            throw new ConfigurationException("Error configuring the indexing field search criterion.", configuration, e);
225        }
226    }
227    
228    /**
229     * Get the operator.
230     * @return the operator.
231     */
232    public Operator getOperator()
233    {
234        return _operator;
235    }
236    
237    public String getFieldId()
238    {
239        return SEARCH_CRITERIA_METADATA_PREFIX + _fullPath;
240    }
241    
242    private String _computeFieldPath(ModelItem definition)
243    {
244        StringBuilder path = new StringBuilder();
245        
246        ModelItemGroup parent = definition.getParent();
247        if (parent != null && !(parent instanceof RepeaterDefinition))
248        {
249            path.append(_computeFieldPath(parent)).append(ModelItem.ITEM_PATH_SEPARATOR);
250        }
251        
252        path.append(definition.getName());
253        return path.toString();
254    }
255    
256    /**
257     * Get the path of field (separated by '/')
258     * @return the path of the field.
259     */
260    public String getFieldPath()
261    {
262        return _fieldPath;
263    }
264    
265    /**
266     * Get the join paths, separated with slashes.
267     * @return the join paths.
268     */
269    public List<String> getJoinPaths()
270    {
271        return Collections.unmodifiableList(_joinPaths);
272    }
273    
274    @Override
275    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
276    {
277        String fieldPath = getFieldPath();
278        List<String> joinPaths = getJoinPaths();
279        Operator operator = customOperator != null ? customOperator : getOperator();
280        
281        if (operator != Operator.EXISTS && isEmpty(value))
282        {
283            return null;
284        }
285        boolean isValueEscaped = BooleanUtils.isTrue((Boolean) contextualParameters.get(QueryBuilder.VALUE_IS_ESCAPED));
286        
287        Query query = null;
288        switch (getType())
289        {
290            case DATE:
291            case DATETIME:
292                query = getDateQuery(value, fieldPath, operator);
293                break;
294            case LONG:
295                query = getLongQuery(value, fieldPath, operator);
296                break;
297            case DOUBLE:
298                query = getDoubleQuery(value, fieldPath, operator);
299                break;
300            case BOOLEAN:
301                query = getBooleanQuery(value, fieldPath, operator);
302                break;
303            case STRING:
304                if (_definition instanceof AbstractMultiValuesProperty property && ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(property.getElementDefinition().getType().getId()))
305                {
306                    query = getContentQuery(value, language, fieldPath, operator);
307                }
308                else
309                {
310                    if (contextualParameters.containsKey(QueryBuilder.MULTILINGUAL_SEARCH))
311                    {
312                        // Force language to null if the search is concern a string field on multilingual contents
313                        query = getStringQuery(value, null, fieldPath, operator, isValueEscaped);
314                    }
315                    else
316                    {
317                        query = getStringQuery(value, language, fieldPath, operator, isValueEscaped);
318                    }
319                }
320                break;
321            case RICH_TEXT:
322                if (contextualParameters.containsKey(QueryBuilder.MULTILINGUAL_SEARCH))
323                {
324                    // Force language to null if the search is concern a string field on multilingual contents
325                    query = getRichTextQuery(value, null, fieldPath, operator, isValueEscaped);
326                }
327                else
328                {
329                    query = getRichTextQuery(value, language, fieldPath, operator, isValueEscaped);
330                }
331                break;
332            case MULTILINGUAL_STRING:
333                query = getStringQuery(value, language, fieldPath, operator, isValueEscaped);
334                break;
335            case USER:
336                query = getUserQuery(value, fieldPath, operator);
337                break;
338            case CONTENT:
339                query = getContentQuery(value, language, fieldPath, operator);
340                break;
341            case REFERENCE:
342                query = getStringQuery(value, language, fieldPath, operator, isValueEscaped);
343                break;
344            case GEOCODE:
345                query = new GeocodeQuery(fieldPath, operator, (Map<String, Integer>) value);
346                break;
347            default:
348                return null;
349        }
350        
351        if (query != null && !joinPaths.isEmpty())
352        {
353            query = new JoinQuery(query, joinPaths);
354        }
355        
356        return query;
357    }
358
359    private static boolean isEmpty(Object value)
360    {
361        return value == null
362            || value instanceof String && ((String) value).length() == 0
363            || value instanceof List && ((List) value).isEmpty();
364    }
365
366    /**
367     * Get a string query.
368     * @param value The value to use for this criterion.
369     * @param language The search language.
370     * @param fieldPath The field path.
371     * @param operator The query operator to use.
372     * @param isValueEscaped <code>true</code> if the given value is already escaped
373     * @return The query.
374     */
375    protected Query getStringQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped)
376    {
377        if (operator == Operator.EXISTS)
378        {
379            return new StringQuery(fieldPath, operator, null, language);
380        }
381        else
382        {
383            return getTextQuery(value, language, fieldPath, operator, isValueEscaped);
384        }
385    }
386    
387    /**
388     * Get a boolean query.
389     * @param value The value to use for this criterion.
390     * @param fieldPath The field path.
391     * @param operator The query operator to use.
392     * @return The query.
393     */
394    protected Query getBooleanQuery(Object value, String fieldPath, Operator operator)
395    {
396        if (operator == Operator.EXISTS)
397        {
398            return new BooleanQuery(fieldPath);
399        }
400        else if (value instanceof Collection<?>)
401        {
402            @SuppressWarnings("unchecked")
403            Collection<Object> values = (Collection<Object>) value;
404            
405            List<Query> queries = new ArrayList<>();
406            for (Object val : values)
407            {
408                queries.add(getBooleanQuery(val, fieldPath, operator));
409            }
410            
411            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
412        }
413        else if (value instanceof String)
414        {
415            Boolean boolValue = Boolean.parseBoolean((String) value);
416            return new BooleanQuery(fieldPath, boolValue);
417        }
418        else
419        {
420            return new BooleanQuery(fieldPath, (Boolean) value);
421        }
422    }
423    
424    /**
425     * Get a double query.
426     * @param value The value to use for this criterion.
427     * @param fieldPath The field path.
428     * @param operator The query operator to use.
429     * @return The query.
430     */
431    protected Query getDoubleQuery(Object value, String fieldPath, Operator operator)
432    {
433        if (operator == Operator.EXISTS)
434        {
435            return new DoubleQuery(fieldPath);
436        }
437        else if (value instanceof Collection<?>)
438        {
439            @SuppressWarnings("unchecked")
440            Collection<Object> values = (Collection<Object>) value;
441            
442            List<Query> queries = new ArrayList<>();
443            for (Object val : values)
444            {
445                queries.add(getDoubleQuery(val, fieldPath, operator));
446            }
447            
448            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
449        }
450        else if (value instanceof String)
451        {
452            Double doubleValue = Double.parseDouble((String) value);
453            return new DoubleQuery(fieldPath, operator, doubleValue);
454        }
455        else if (value instanceof Integer)
456        {
457            return new DoubleQuery(fieldPath, operator, ((Integer) value).doubleValue());
458        }
459        else if (value instanceof Number)
460        {
461            Number valueAsNumber = (Number) value;
462            Double valueAsDouble = valueAsNumber.doubleValue();
463            // We check that the cast to long was safe
464            if (valueAsNumber.toString().equals(valueAsDouble.toString()))
465            {
466                return new DoubleQuery(fieldPath, operator, valueAsDouble);
467            }
468            else
469            {
470                throw new NumberFormatException("Failed to convert the value " + value + " to Double. The value is out of bound.");
471            }
472        }
473        else
474        {
475            throw new NumberFormatException("Failed to convert the value " + value + " to Double.");
476        }
477    }
478    
479    /**
480     * Get a long query.
481     * @param value The value to use for this criterion.
482     * @param fieldPath The field path.
483     * @param operator The query operator to use.
484     * @return The query.
485     */
486    protected Query getLongQuery(Object value, String fieldPath, Operator operator)
487    {
488        if (operator == Operator.EXISTS)
489        {
490            return new LongQuery(fieldPath);
491        }
492        else if (value instanceof Collection<?>)
493        {
494            @SuppressWarnings("unchecked")
495            Collection<Object> values = (Collection<Object>) value;
496            
497            List<Query> queries = new ArrayList<>();
498            for (Object val : values)
499            {
500                queries.add(getLongQuery(val, fieldPath, operator));
501            }
502            
503            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
504        }
505        else if (value instanceof String)
506        {
507            return new LongQuery(fieldPath, operator, Long.parseLong((String) value));
508        }
509        else if (value instanceof Number)
510        {   
511            Number valueAsNumber = (Number) value;
512            Long valueAsLong = valueAsNumber.longValue();
513            // We check that the cast to long was safe
514            if (valueAsNumber.toString().equals(valueAsLong.toString()))
515            {
516                return new LongQuery(fieldPath, operator, valueAsLong);
517            }
518            else
519            {
520                throw new NumberFormatException("Failed to convert the value " + value + " to Long. The value is out of bound.");
521            }
522        }
523        else
524        {
525            throw new NumberFormatException("Failed to convert the value " + value + " to Long.");
526        }
527    }
528    
529    /**
530     * Get a date query.
531     * @param value The value to use for this criterion.
532     * @param fieldPath The field path.
533     * @param operator The query operator to use.
534     * @return The query.
535     */
536    protected Query getDateQuery(Object value, String fieldPath, Operator operator)
537    {
538        if (operator == Operator.EXISTS)
539        {
540            return new DateQuery(fieldPath);
541        }
542        else if (value instanceof Collection<?>)
543        {
544            @SuppressWarnings("unchecked")
545            Collection<Object> values = (Collection<Object>) value;
546            
547            List<Query> queries = new ArrayList<>();
548            for (Object val : values)
549            {
550                queries.add(getDateQuery(val, fieldPath, operator));
551            }
552            
553            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
554        }
555        else 
556        {
557            return _getSingleDateQuery((String) value, fieldPath, operator);
558        }
559    }
560    
561    private Query _getSingleDateQuery(String value, String fieldPath, Operator operator)
562    {
563        return new DateQuery(fieldPath, operator, AdaptableDateParser.parse(value));
564    }
565    
566    /**
567     * Get a content query.
568     * @param value The value to use for this criterion.
569     * @param language The search language.
570     * @param fieldPath The field path.
571     * @param operator The query operator to use.
572     * @return The query.
573     */
574    protected Query getContentQuery(Object value, String language, String fieldPath, Operator operator)
575    {        
576        boolean isMultipleOperandAnd = operator == Operator.NE ? !_isMultipleOperandAnd : _isMultipleOperandAnd; // reverse it if NE
577        return new ContentQuery(fieldPath, operator, value, _resolver, _contentHelper, _hierarchicalReferenceTablesHelper, false, isMultipleOperandAnd);
578    }
579    
580    /**
581     * Get a user query
582     * @param value The value to use for this criterion.
583     * @param fieldPath The field path.
584     * @param operator The query operator to use.
585     * @return The query.
586     */
587    @SuppressWarnings("unchecked")
588    protected Query getUserQuery(Object value, String fieldPath, Operator operator)
589    {
590        List<UserIdentity> users;
591        if (value instanceof Map<?, ?>)
592        {
593            users = List.of(_userHelper.json2userIdentity((Map<String, Object>) value));
594        }
595        else if (value instanceof Collection< ? >)
596        {
597            users = ((Collection<Map<String, Object>>) value).stream()
598                                                             .map(_userHelper::json2userIdentity).toList();
599        }
600        else
601        {
602            throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type USER cannot be cast to a Map or a Collection");
603        }
604        return new UsersQuery(fieldPath + "_s", operator, users);
605    }
606
607    /**
608     * Get a geocode query.
609     * @param value The value to use for this criterion.
610     * @param fieldPath The field path.
611     * @param operator The query operator to use.
612     * @return The query.
613     */
614    protected Query getGeocodeQuery(Object value, String fieldPath, Operator operator)
615    {
616        if (operator == Operator.EXISTS)
617        {
618            return new GeocodeQuery(fieldPath);
619        }
620        else if (value instanceof Collection<?>)
621        {
622            @SuppressWarnings("unchecked")
623            Collection<Map<String, Integer>> values = (Collection<Map<String, Integer>>) value;
624            
625            List<Query> queries = new ArrayList<>();
626            for (Object val : values)
627            {
628                queries.add(getGeocodeQuery(val, fieldPath, operator));
629            }
630            
631            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
632        }
633        else if (value instanceof Map<?, ?>)
634        {
635            return new GeocodeQuery(fieldPath, operator, (Map<String, Integer>) value);
636        }
637        else
638        {
639            throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type GEOCODE cannot be cast to a Map or a Collection");
640        }
641    }
642    
643    /**
644     * Get a rich text query.
645     * @param value The value to use for this criterion.
646     * @param language The search language.
647     * @param fieldPath The field path.
648     * @param operator The query operator to use.
649     * @param isValueEscaped <code>true</code> if the given value is already escaped
650     * @return The query.
651     */
652    protected Query getRichTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped)
653    {
654        if (operator == Operator.EXISTS)
655        {
656            return new RichTextQuery(fieldPath, operator, null, language);
657        }
658        else
659        {
660            return getTextQuery(value, language, fieldPath, operator, isValueEscaped);
661        }
662    }
663    
664    /**
665     * Get a text query.
666     * @param value The value to use for this criterion.
667     * @param language The search language.
668     * @param fieldPath The field path.
669     * @param operator The query operator to use.
670     * @param isValueEscaped <code>true</code> if the given value is already escaped
671     * @return The query.
672     */
673    protected Query getTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped)
674    {
675        if (value instanceof Collection<?>)
676        {
677            @SuppressWarnings("unchecked")
678            Collection<Object> values = (Collection<Object>) value;
679            
680            List<Query> queries = new ArrayList<>();
681            for (Object val : values)
682            {
683                queries.add(getStringQuery(val, language, fieldPath, operator, isValueEscaped));
684            }
685            
686            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
687        }
688        else if (operator.equals(Operator.LIKE))
689        {
690            String stringValue = (String) value;
691            
692            if (StringUtils.isNotEmpty(stringValue))
693            {
694                boolean doNotUseLanguage = getType() != MetadataType.MULTILINGUAL_STRING;
695                return new StringQuery(fieldPath, operator, stringValue, doNotUseLanguage ? null : language, isValueEscaped);
696            }
697            
698            return null;
699        }
700        else
701        {
702            return new StringQuery(fieldPath, operator, (String) value, language, isValueEscaped);
703        }
704    }
705    
706    @Override
707    public SearchField getSearchField()
708    {
709        return _searchHelper.getMetadataSearchField(getJoinPaths(), getFieldPath(), getType(), _isTypeContentWithMultilingualTitle);
710    }
711    
712    private void _configureTypeContentWithMultilingualTitle(ModelItem modelItem)
713    {
714        _isTypeContentWithMultilingualTitle = _searchHelper.isTitleMultilingual(modelItem);
715    }
716    
717    /**
718     * Configure the criterion operator.
719     * @param configuration the global criterion configuration.
720     * @param definition the metadata definition.
721     * @throws ConfigurationException if an error occurs.
722     */
723    @SuppressWarnings("unchecked")
724    private void _configureOperator(Configuration configuration, ElementDefinition definition) throws ConfigurationException
725    {
726        try
727        {
728            String definitionTypeId = definition.getType().getId();
729            if (definition instanceof AbstractMultiValuesProperty property)
730            {
731                definitionTypeId = property.getElementDefinition().getType().getId();
732            }
733            
734            String op = configuration.getChild("test-operator").getValue("");
735            if (StringUtils.isNotBlank(op))
736            {
737                _operator = Operator.fromName(op);
738            }
739            else
740            {
741                if (definition.getCriterionEnumerator(configuration, _enumeratorManager) == null
742                    && (org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(definitionTypeId)
743                        || ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(definitionTypeId)
744                        || ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(definitionTypeId)
745                        || ModelItemTypeConstants.REFERENCE_ELEMENT_TYPE_ID.equals(definitionTypeId)))
746                {
747                    _operator = Operator.SEARCH;
748                }
749                else
750                {
751                    _operator = Operator.EQ;
752                }
753            }
754        }
755        catch (IllegalArgumentException e)
756        {
757            throw new ConfigurationException("Invalid operator", configuration, e);
758        }
759    }
760    
761    private void _configureLabel(Configuration configuration, ModelItem modelItem)
762    {
763        setLabel(_configureI18nizableText(configuration.getChild("label", false), modelItem.getLabel()));
764    }
765    
766    private void _configureDescription(Configuration configuration, ModelItem modelItem)
767    {
768        setDescription(_configureI18nizableText(configuration.getChild("description", false), modelItem.getDescription()));
769    }
770}