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