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