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.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
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.StringUtils;
035import org.slf4j.Logger;
036
037import org.ametys.cms.contenttype.ContentConstants;
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.MetadataDefinition;
040import org.ametys.cms.contenttype.MetadataType;
041import org.ametys.cms.contenttype.indexing.CustomIndexingField;
042import org.ametys.cms.contenttype.indexing.IndexingField;
043import org.ametys.cms.contenttype.indexing.IndexingModel;
044import org.ametys.cms.contenttype.indexing.MetadataIndexingField;
045import org.ametys.cms.search.SearchField;
046import org.ametys.cms.search.model.SystemProperty;
047import org.ametys.cms.search.model.SystemProperty.EnumeratorDefinition;
048import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
049import org.ametys.cms.search.model.SystemSearchCriterion;
050import org.ametys.cms.search.query.JoinQuery;
051import org.ametys.cms.search.query.Query;
052import org.ametys.cms.search.query.Query.Operator;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.runtime.parameter.Enumerator;
055import org.ametys.runtime.parameter.StaticEnumerator;
056import org.ametys.runtime.parameter.Validator;
057import org.ametys.runtime.plugin.component.LogEnabled;
058import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
059
060/**
061 * This class is a search criteria on a system property (author, lastModified, with-comments, ...)
062 */
063public class SystemSearchUICriterion extends AbstractSearchUICriterion implements SystemSearchCriterion, Contextualizable, LogEnabled, Disposable
064{
065    
066    /** Prefix for id of system property search criteria */
067    public static final String SEARCH_CRITERION_SYSTEM_PREFIX = "property-";
068    
069    /** ComponentManager for {@link Validator}s. */
070    protected ThreadSafeComponentManager<Validator> _validatorManager;
071    /** ComponentManager for {@link Enumerator}s. */
072    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
073    
074    /** The system property extension point. */
075    protected SystemPropertyExtensionPoint _systemPropEP;
076    
077    /** The join paths */
078    protected List<String> _joinPaths;
079    
080    private Operator _operator;
081    private SystemProperty _systemProperty;
082    private Set<String> _contentTypes;
083    private String _fullPath;
084    
085    private ServiceManager _manager;
086    private Logger _logger;
087    private Context _context;
088
089    @Override
090    public void contextualize(Context context) throws ContextException
091    {
092        _context = context;
093    }
094    
095    @Override
096    public void setLogger(Logger logger)
097    {
098        _logger = logger;
099    }
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        super.service(manager);
105        _manager = manager;
106        _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
107    }
108    
109    @Override
110    public void dispose()
111    {
112        _validatorManager.dispose();
113        _validatorManager = null;
114        _enumeratorManager.dispose();
115        _enumeratorManager = null;
116    }
117    
118    @Override
119    public void configure(Configuration configuration) throws ConfigurationException
120    {
121        try
122        {
123            _validatorManager = new ThreadSafeComponentManager<>();
124            _validatorManager.setLogger(_logger);
125            _validatorManager.contextualize(_context);
126            _validatorManager.service(_manager);
127            
128            _enumeratorManager = new ThreadSafeComponentManager<>();
129            _enumeratorManager.setLogger(_logger);
130            _enumeratorManager.contextualize(_context);
131            _enumeratorManager.service(_manager);
132            
133            _fullPath = configuration.getChild("systemProperty").getAttribute("name");
134            int pos = _fullPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR);
135            
136            String systemPropertyId = pos > -1 ? _fullPath.substring(pos + ContentConstants.METADATA_PATH_SEPARATOR.length()) : _fullPath;
137            _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq"));
138            
139            if (!_systemPropEP.isSearchable(systemPropertyId))
140            {
141                throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not searchable.");
142            }
143            
144            String contentTypeId = configuration.getChild("contentTypes").getAttribute("baseId", null);
145            
146            String joinPath = pos > -1 ? _fullPath.substring(0, pos) : "";
147            _joinPaths = _configureJoinPaths(joinPath, contentTypeId);
148            _systemProperty = _systemPropEP.getExtension(systemPropertyId);
149            
150            setId(SEARCH_CRITERION_SYSTEM_PREFIX + _fullPath + "-" + _operator.getName());
151            setGroup(_configureI18nizableText(configuration.getChild("group", false), null));
152            
153            String validatorRole = "validator";
154            if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration))
155            {
156                validatorRole = null;
157            }
158            
159            _contentTypes = new HashSet<>();
160            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
161            {
162                _contentTypes.add(cTypeConf.getAttribute("id"));
163            }
164            
165            setLabel(_systemProperty.getLabel());
166            setDescription(_systemProperty.getDescription());
167            setType(_systemProperty.getType());
168            // Multiple defaults to false even for a multiple property.
169            setMultiple(configuration.getAttributeAsBoolean("multiple", false));
170            
171            // TODO Add the current criterion configuration.
172            String enumeratorRole = null;
173            EnumeratorDefinition enumDef = _systemProperty.getEnumeratorDefinition(_contentTypes, configuration);
174            if (enumDef != null)
175            {
176                enumeratorRole = _initializeEnumerator(enumDef);
177            }
178            
179            String defaultWidget = _systemProperty.getWidget();
180            if ("edition.textarea".equals(defaultWidget))
181            {
182                defaultWidget = null;
183            }
184            else if (defaultWidget == null && _systemProperty.getType() == MetadataType.BOOLEAN)
185            {
186                defaultWidget = "edition.boolean-combobox";
187            }
188            
189            setWidget(configuration.getChild("widget").getValue(defaultWidget));
190            setWidgetParameters(_configureWidgetParameters(configuration.getChild("widget-params", false), _systemProperty.getWidgetParameters()));
191            
192            // Potentially replace the standard label and description by the custom ones.
193            I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null);
194            if (userLabel != null)
195            {
196                setLabel(userLabel);
197            }
198            I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null);
199            if (userDescription != null)
200            {
201                setDescription(userDescription);
202            }
203            
204            configureUIProperties(configuration);
205            configureValues(configuration);
206            
207            if (enumeratorRole != null)
208            {
209                _enumeratorManager.initialize();
210                setEnumerator(_enumeratorManager.lookup(enumeratorRole));
211            }
212            
213            if (validatorRole != null)
214            {
215                _validatorManager.initialize();
216                setValidator(_validatorManager.lookup(validatorRole));
217            }
218        }
219        catch (Exception e)
220        {
221            throw new ConfigurationException("Error configuring the system search criterion.", configuration, e);
222        }
223    }
224    
225    /**
226     * Get the operator.
227     * @return the operator.
228     */
229    public Operator getOperator()
230    {
231        return _operator;
232    }
233    
234    public String getFieldId()
235    {
236        return SEARCH_CRITERION_SYSTEM_PREFIX + _systemProperty.getId();
237    }
238    
239    /**
240     * Get id of this system property
241     * @return The system property's id
242     */
243    public String getSystemPropertyId()
244    {
245        return _systemProperty.getId();
246    }
247    
248    @Override
249    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
250    {
251        if (value == null || (value instanceof String && ((String) value).length() == 0) || (value instanceof List && ((List) value).isEmpty()))
252        {
253            return null;
254        }
255        
256        Operator operator = customOperator != null ? customOperator : getOperator();
257        
258        Query query = _systemProperty.getQuery(value, operator, language, contextualParameters);
259        
260        if (query != null && !_joinPaths.isEmpty())
261        {
262            query = new JoinQuery(query, _joinPaths);
263        }
264        
265        return query;
266    }
267    
268    @Override
269    public SearchField getSearchField()
270    {
271        // No search field for a joined field.
272        if (_joinPaths.isEmpty())
273        {
274            return _systemProperty.getSearchField();
275        }
276        
277        return null;
278    }
279    
280    private Map<String, I18nizableText> _configureWidgetParameters(Configuration config, Map<String, I18nizableText> defaultParams) throws ConfigurationException
281    {
282        if (config != null)
283        {
284            Map<String, I18nizableText> widgetParams = new HashMap<>();
285            
286            Configuration[] params = config.getChildren("param");
287            for (Configuration paramConfig : params)
288            {
289                boolean i18nSupported = paramConfig.getAttributeAsBoolean("i18n", false);
290                if (i18nSupported)
291                {
292                    String catalogue = paramConfig.getAttribute("catalogue", null);
293                    widgetParams.put(paramConfig.getAttribute("name"), new I18nizableText(catalogue, paramConfig.getValue()));
294                }
295                else
296                {
297                    widgetParams.put(paramConfig.getAttribute("name"), new I18nizableText(paramConfig.getValue("")));
298                }
299            }
300            
301            return widgetParams;
302        }
303        else
304        {
305            return defaultParams;
306        }
307    }
308    
309    /**
310     * Configure the join paths.
311     * @param joinPath The full join path.
312     * @param contentTypeId The base content type ID, can be null.
313     * @return The list of join paths.
314     * @throws ConfigurationException If the join paths are missconfigured
315     */
316    private List<String> _configureJoinPaths(String joinPath, String contentTypeId) throws ConfigurationException
317    {
318        List<String> joinPaths = new ArrayList<>();
319        
320        if (StringUtils.isNotBlank(joinPath))
321        {
322            if (StringUtils.isBlank(contentTypeId))
323            {
324                throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without a base content type.");
325            }
326            
327            ContentType cType = _cTypeEP.getExtension(contentTypeId);
328            IndexingModel indexingModel = cType.getIndexingModel();
329            
330            String[] pathSegments = StringUtils.split(joinPath, ContentConstants.METADATA_PATH_SEPARATOR);
331            String firstField = pathSegments[0];
332            IndexingField indexingField = indexingModel.getField(firstField);
333            
334            if (indexingField == null)
335            {
336                throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown indexing field: " + firstField);
337            }
338            
339            String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0];
340            
341            MetadataType type = _computeJoinPaths(indexingField, remainingPathSegments, joinPaths);
342            
343            // The final definition must be a content (to be able to extract a system property from it).
344            if (type != MetadataType.CONTENT)
345            {
346                throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content.");
347            }
348        }
349        
350        return joinPaths;
351    }
352    
353    /**
354     * Get the indexing field type and compute the join paths.
355     * @param indexingField The initial indexing field
356     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
357     * @param joinPaths The consecutive's path in case of joint to access the field/metadata
358     * @return The metadata definition or null if not found
359     * @throws ConfigurationException If an error occurs.
360     */
361    private MetadataType _computeJoinPaths(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException
362    {
363        if (indexingField instanceof MetadataIndexingField)
364        {
365            MetadataDefinition definition = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true);
366            
367            return definition.getType();
368        }
369        else if (indexingField instanceof CustomIndexingField)
370        {
371            // Remaining path segments should be exactly 1 (the last path segment being the property).
372            if (remainingPathSegments.length != 1)
373            {
374                throw new ConfigurationException("The remaining path of the custom indexing field '" + indexingField.getName() + "' must represent a system property: " + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR));
375            }
376            else
377            {
378                // No more recursion
379                return indexingField.getType();
380            }
381        }
382        else
383        {
384            throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")");
385        }
386    }
387    
388    private String _initializeEnumerator(EnumeratorDefinition enumDef)
389    {
390        String role = null;
391        
392        if (enumDef.isStatic())
393        {
394            StaticEnumerator enumerator = new StaticEnumerator();
395            enumDef.getStaticEntries().entrySet().forEach(entry -> enumerator.add(entry.getValue(), entry.getKey()));
396            setEnumerator(enumerator);
397        }
398        else
399        {
400            role = "enumerator";
401            _enumeratorManager.addComponent("cms", null, role, enumDef.getEnumeratorClass(), enumDef.getConfiguration());
402        }
403        
404        return role;
405    }
406    
407}