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