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