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