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