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(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 = Boolean.parseBoolean((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 = Double.parseDouble((String) value);
461            return new DoubleQuery(fieldPath, operator, doubleValue);
462        }
463        else if (value instanceof Integer)
464        {
465            return new DoubleQuery(fieldPath, operator, ((Integer) value).doubleValue());
466        }
467        else if (value instanceof Number)
468        {
469            Number valueAsNumber = (Number) value;
470            Double valueAsDouble = valueAsNumber.doubleValue();
471            // We check that the cast to long was safe
472            if (valueAsNumber.toString().equals(valueAsDouble.toString()))
473            {
474                return new DoubleQuery(fieldPath, operator, valueAsDouble);
475            }
476            else
477            {
478                throw new NumberFormatException("Failed to convert the value " + value + " to Double. The value is out of bound.");
479            }
480        }
481        else
482        {
483            throw new NumberFormatException("Failed to convert the value " + value + " to Double.");
484        }
485    }
486    
487    /**
488     * Get a long query.
489     * @param value The value to use for this criterion.
490     * @param fieldPath The field path.
491     * @param operator The query operator to use.
492     * @return The query.
493     */
494    protected Query getLongQuery(Object value, String fieldPath, Operator operator)
495    {
496        if (operator == Operator.EXISTS)
497        {
498            return new LongQuery(fieldPath);
499        }
500        else if (value instanceof Collection<?>)
501        {
502            @SuppressWarnings("unchecked")
503            Collection<Object> values = (Collection<Object>) value;
504            
505            List<Query> queries = new ArrayList<>();
506            for (Object val : values)
507            {
508                queries.add(getLongQuery(val, fieldPath, operator));
509            }
510            
511            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
512        }
513        else if (value instanceof String)
514        {
515            return new LongQuery(fieldPath, operator, Long.parseLong((String) value));
516        }
517        else if (value instanceof Number)
518        {   
519            Number valueAsNumber = (Number) value;
520            Long valueAsLong = valueAsNumber.longValue();
521            // We check that the cast to long was safe
522            if (valueAsNumber.toString().equals(valueAsLong.toString()))
523            {
524                return new LongQuery(fieldPath, operator, valueAsLong);
525            }
526            else
527            {
528                throw new NumberFormatException("Failed to convert the value " + value + " to Long. The value is out of bound.");
529            }
530        }
531        else
532        {
533            throw new NumberFormatException("Failed to convert the value " + value + " to Long.");
534        }
535    }
536    
537    /**
538     * Get a date query.
539     * @param value The value to use for this criterion.
540     * @param fieldPath The field path.
541     * @param operator The query operator to use.
542     * @return The query.
543     */
544    protected Query getDateQuery(Object value, String fieldPath, Operator operator)
545    {
546        if (operator == Operator.EXISTS)
547        {
548            return new DateQuery(fieldPath);
549        }
550        else if (value instanceof Collection<?>)
551        {
552            @SuppressWarnings("unchecked")
553            Collection<Object> values = (Collection<Object>) value;
554            
555            List<Query> queries = new ArrayList<>();
556            for (Object val : values)
557            {
558                queries.add(getDateQuery(val, fieldPath, operator));
559            }
560            
561            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
562        }
563        else 
564        {
565            return _getSingleDateQuery((String) value, fieldPath, operator);
566        }
567    }
568    
569    private Query _getSingleDateQuery(String value, String fieldPath, Operator operator)
570    {
571        return new DateQuery(fieldPath, operator, AdaptableDateParser.parse(value));
572    }
573    
574    /**
575     * Get a content query.
576     * @param value The value to use for this criterion.
577     * @param language The search language.
578     * @param fieldPath The field path.
579     * @param operator The query operator to use.
580     * @return The query.
581     */
582    protected Query getContentQuery(Object value, String language, String fieldPath, Operator operator)
583    {        
584        boolean isMultipleOperandAnd = operator == Operator.NE ? !_isMultipleOperandAnd : _isMultipleOperandAnd; // reverse it if NE
585        return new ContentQuery(fieldPath, operator, value, _resolver, _contentHelper, _hierarchicalReferenceTablesHelper, false, isMultipleOperandAnd);
586    }
587    
588    /**
589     * Get a user query
590     * @param value The value to use for this criterion.
591     * @param fieldPath The field path.
592     * @param operator The query operator to use.
593     * @return The query.
594     */
595    @SuppressWarnings("unchecked")
596    protected Query getUserQuery(Object value, String fieldPath, Operator operator)
597    {
598        Collection<UserIdentity> users;
599        if (value instanceof Map<?, ?>)
600        {
601            users = Collections.singleton(_userHelper.json2userIdentity((Map<String, ? extends Object>) value));
602        }
603        else if (value instanceof Collection< ? >)
604        {
605            users = ((Collection<Map<String, ? extends Object>>) value).stream()
606                .map(_userHelper::json2userIdentity)
607                .collect(Collectors.toList());
608        }
609        else
610        {
611            throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type USER cannot be cast to a Map or a Collection");
612        }
613        return new UsersQuery(fieldPath + "_s", operator, users);
614    }
615
616    /**
617     * Get a geocode query.
618     * @param value The value to use for this criterion.
619     * @param fieldPath The field path.
620     * @param operator The query operator to use.
621     * @return The query.
622     */
623    protected Query getGeocodeQuery(Object value, String fieldPath, Operator operator)
624    {
625        if (operator == Operator.EXISTS)
626        {
627            return new GeocodeQuery(fieldPath);
628        }
629        else if (value instanceof Collection<?>)
630        {
631            @SuppressWarnings("unchecked")
632            Collection<Map<String, Integer>> values = (Collection<Map<String, Integer>>) value;
633            
634            List<Query> queries = new ArrayList<>();
635            for (Object val : values)
636            {
637                queries.add(getGeocodeQuery(val, fieldPath, operator));
638            }
639            
640            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
641        }
642        else if (value instanceof Map<?, ?>)
643        {
644            return new GeocodeQuery(fieldPath, operator, (Map<String, Integer>) value);
645        }
646        else
647        {
648            throw new IllegalArgumentException("The value for the field '" + fieldPath + "' of type GEOCODE cannot be cast to a Map or a Collection");
649        }
650    }
651    
652    /**
653     * Get a rich text query.
654     * @param value The value to use for this criterion.
655     * @param language The search language.
656     * @param fieldPath The field path.
657     * @param operator The query operator to use.
658     * @param isValueEscaped <code>true</code> if the given value is already escaped
659     * @return The query.
660     */
661    protected Query getRichTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped)
662    {
663        if (operator == Operator.EXISTS)
664        {
665            return new RichTextQuery(fieldPath, operator, null, language);
666        }
667        else
668        {
669            return getTextQuery(value, language, fieldPath, operator, isValueEscaped);
670        }
671    }
672    
673    /**
674     * Get a text query.
675     * @param value The value to use for this criterion.
676     * @param language The search language.
677     * @param fieldPath The field path.
678     * @param operator The query operator to use.
679     * @param isValueEscaped <code>true</code> if the given value is already escaped
680     * @return The query.
681     */
682    protected Query getTextQuery(Object value, String language, String fieldPath, Operator operator, boolean isValueEscaped)
683    {
684        if (value instanceof Collection<?>)
685        {
686            @SuppressWarnings("unchecked")
687            Collection<Object> values = (Collection<Object>) value;
688            
689            List<Query> queries = new ArrayList<>();
690            for (Object val : values)
691            {
692                queries.add(getStringQuery(val, language, fieldPath, operator, isValueEscaped));
693            }
694            
695            return _isMultipleOperandAnd ? new AndQuery(queries) : new OrQuery(queries);
696        }
697        else if (operator.equals(Operator.LIKE))
698        {
699            String stringValue = (String) value;
700            
701            if (StringUtils.isNotEmpty(stringValue))
702            {
703                boolean doNotUseLanguage = getType() != MetadataType.MULTILINGUAL_STRING;
704                return new StringQuery(fieldPath, operator, stringValue, doNotUseLanguage ? null : language, isValueEscaped);
705            }
706            
707            return null;
708        }
709        else
710        {
711            return new StringQuery(fieldPath, operator, (String) value, language, isValueEscaped);
712        }
713    }
714    
715    @Override
716    public SearchField getSearchField()
717    {
718        return _searchHelper.getMetadataSearchField(getJoinPaths(), getFieldPath(), getType(), _isTypeContentWithMultilingualTitle);
719    }
720    
721    private void _configureTypeContentWithMultilingualTitle(MetadataDefinition finalDefinition)
722    {
723        _isTypeContentWithMultilingualTitle = _searchHelper.isTitleMultilingual(finalDefinition);
724    }
725    
726    /**
727     * Configure the criterion operator.
728     * @param configuration the global criterion configuration.
729     * @param definition the metadata definition.
730     * @throws ConfigurationException if an error occurs.
731     */
732    private void _configureOperator(Configuration configuration, MetadataDefinition definition) throws ConfigurationException
733    {
734        try
735        {
736            String op = configuration.getChild("test-operator").getValue("");
737            if (StringUtils.isNotBlank(op))
738            {
739                _operator = Operator.fromName(op);
740            }
741            else if (definition != null && definition.getEnumerator() == null
742                && (definition.getType() == MetadataType.STRING || definition.getType() == MetadataType.MULTILINGUAL_STRING || definition.getType() == MetadataType.RICH_TEXT || definition.getType() == MetadataType.REFERENCE))
743            {
744                _operator = Operator.SEARCH;
745            }
746            else
747            {
748                _operator = Operator.EQ;
749            }
750        }
751        catch (IllegalArgumentException e)
752        {
753            throw new ConfigurationException("Invalid operator", configuration, e);
754        }
755    }
756    
757    private void _configureLabel(Configuration configuration, IndexingField indexingField, String fieldName, MetadataDefinition definition)
758    {
759        I18nizableText defaultValue = new I18nizableText(fieldName);
760        if (definition != null)
761        {
762            defaultValue = definition.getLabel();
763        }
764        else if (indexingField != null && indexingField.getLabel() != null)
765        {
766            defaultValue = indexingField.getLabel();
767        }
768        
769        setLabel(_configureI18nizableText(configuration.getChild("label", false), defaultValue));
770    }
771    
772    private void _configureDescription(Configuration configuration, IndexingField indexingField, String fieldName, MetadataDefinition definition)
773    {
774        I18nizableText defaultValue = new I18nizableText(fieldName);
775        if (definition != null)
776        {
777            defaultValue = definition.getDescription();
778        }
779        else if (indexingField != null && indexingField.getDescription() != null)
780        {
781            defaultValue = indexingField.getDescription();
782        }
783        
784        setDescription(_configureI18nizableText(configuration.getChild("description", false), defaultValue));
785    }
786
787    /**
788     * Get the metadata definition from the indexing field. Can be null if the last indexing field is a custom indexing field.
789     * @param indexingField The initial indexing field
790     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
791     * @param joinPaths The consecutive's path in case of joint to access the field/metadata
792     * @return The metadata definition or null if not found
793     * @throws ConfigurationException If an error occurs.
794     */
795    protected MetadataDefinition getMetadataDefinition(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException
796    {
797        if (indexingField instanceof MetadataIndexingField)
798        {
799            return getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, false);
800        }
801        else if (indexingField instanceof CustomIndexingField)
802        {
803            if (remainingPathSegments.length > 0)
804            {
805                throw new ConfigurationException("The custom indexing field '" + indexingField.getName() + "' can not have remaining path: " + StringUtils.join(remainingPathSegments, ModelItem.ITEM_PATH_SEPARATOR));
806            }
807            else
808            {
809                // No more recursion
810                setType(indexingField.getType());
811                // No metadata definition for a custom indexing field
812                return null;
813            }
814        }
815        else
816        {
817            throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")");
818        }
819    }
820    
821    /**
822     * Get the field's path without join paths
823     * @param indexingField The initial indexing field
824     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
825     * @return the field's path
826     * @throws ConfigurationException If an error occurs.
827     */
828    protected String getFieldPath(IndexingField indexingField, String[] remainingPathSegments) throws ConfigurationException
829    {
830        StringBuilder currentMetaPath = new StringBuilder();
831        currentMetaPath.append(indexingField.getName());
832        
833        if (indexingField instanceof MetadataIndexingField)
834        {
835            MetadataDefinition definition = ((MetadataIndexingField) indexingField).getMetadataDefinition();
836            
837            for (int i = 0; i < remainingPathSegments.length && definition != null; i++)
838            {
839                if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT)
840                {
841                    currentMetaPath = new StringBuilder();
842                    currentMetaPath.append(remainingPathSegments[i]);
843                    
844                    String refCTypeId = definition.getContentType();
845                    if (refCTypeId != null && _cTypeEP.hasExtension(refCTypeId))
846                    {
847                        ContentType refCType = _cTypeEP.getExtension(refCTypeId);
848                        IndexingModel refIndexingModel = refCType.getIndexingModel();
849                        
850                        IndexingField refIndexingField = refIndexingModel.getField(remainingPathSegments[i]);
851                        if (refIndexingField == null)
852                        {
853                            throw new ConfigurationException("Indexing field search criteria with path '" + StringUtils.join(remainingPathSegments, ModelItem.ITEM_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + remainingPathSegments[i]);
854                        }
855                        
856                        definition = refCType.getMetadataDefinition(remainingPathSegments[i]);
857                    }
858                }
859                else
860                {
861                    if (definition instanceof RepeaterDefinition)
862                    {
863                        // Add path to repeater from current content type or last repeater to join paths
864                        currentMetaPath = new StringBuilder();
865                        currentMetaPath.append(remainingPathSegments[i]);
866                    }
867                    else
868                    {
869                        currentMetaPath.append(ModelItem.ITEM_PATH_SEPARATOR).append(remainingPathSegments[i]);
870                    }
871                    definition = definition.getMetadataDefinition(remainingPathSegments[i]);
872                }
873            }
874            
875            // If the criteria is a join on several levels, the path of the metadata is the path from the last level of joint
876            // Ex: {!ametys join=[join paths separated by arrows] q=[metadataPath]:"content://xxxx"}
877            // -> {!ametys join=address/city_s_dv->links/department_s_dv q=links/state_ss:"content://xxxx"}
878            return currentMetaPath.toString();
879        }
880        else if (indexingField instanceof CustomIndexingField)
881        {
882            if (remainingPathSegments.length > 0)
883            {
884                throw new ConfigurationException("The custom indexing field '" + indexingField.getName() + "' can not have remaining path: " + StringUtils.join(remainingPathSegments, ModelItem.ITEM_PATH_SEPARATOR));
885            }
886            else
887            {
888                return indexingField.getName();
889            }
890        }
891        else
892        {
893            throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")");
894        }
895    }
896    
897}