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