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