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