001/*
002 *  Copyright 2013 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.search.ui.model.impl;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027import org.apache.avalon.framework.activity.Disposable;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.configuration.DefaultConfiguration;
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.collections4.CollectionUtils;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.slf4j.Logger;
040
041import org.ametys.cms.contenttype.ContentConstants;
042import org.ametys.cms.contenttype.ContentType;
043import org.ametys.cms.contenttype.MetadataDefinition;
044import org.ametys.cms.contenttype.MetadataType;
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.model.SystemProperty;
051import org.ametys.cms.search.model.SystemProperty.EnumeratorDefinition;
052import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
053import org.ametys.cms.search.model.SystemSearchCriterion;
054import org.ametys.cms.search.query.JoinQuery;
055import org.ametys.cms.search.query.Query;
056import org.ametys.cms.search.query.Query.Operator;
057import org.ametys.cms.search.solr.field.JoinedSystemSearchField;
058import org.ametys.runtime.i18n.I18nizableText;
059import org.ametys.runtime.parameter.Enumerator;
060import org.ametys.runtime.parameter.StaticEnumerator;
061import org.ametys.runtime.parameter.Validator;
062import org.ametys.runtime.plugin.component.LogEnabled;
063import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
064
065/**
066 * This class is a search criteria on a system property (author, lastModified, with-comments, ...)
067 */
068public class SystemSearchUICriterion extends AbstractSearchUICriterion implements SystemSearchCriterion, Contextualizable, LogEnabled, Disposable
069{
070    
071    /** Prefix for id of system property search criteria */
072    public static final String SEARCH_CRITERION_SYSTEM_PREFIX = "property-";
073    
074    /** ComponentManager for {@link Validator}s. */
075    protected ThreadSafeComponentManager<Validator> _validatorManager;
076    /** ComponentManager for {@link Enumerator}s. */
077    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
078    
079    /** The system property extension point. */
080    protected SystemPropertyExtensionPoint _systemPropEP;
081    
082    /** The join paths */
083    protected List<String> _joinPaths;
084    
085    private Operator _operator;
086    private SystemProperty _systemProperty;
087    private Set<String> _contentTypes;
088    private String _fullPath;
089    
090    private ServiceManager _manager;
091    private Logger _logger;
092    private Context _context;
093
094
095    @Override
096    public void contextualize(Context context) throws ContextException
097    {
098        _context = context;
099    }
100    
101    @Override
102    public void setLogger(Logger logger)
103    {
104        _logger = logger;
105    }
106    
107    @Override
108    public void service(ServiceManager manager) throws ServiceException
109    {
110        super.service(manager);
111        _manager = manager;
112        _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
113    }
114    
115    @Override
116    public void dispose()
117    {
118        _validatorManager.dispose();
119        _validatorManager = null;
120        _enumeratorManager.dispose();
121        _enumeratorManager = null;
122    }
123    
124    @Override
125    public void configure(Configuration configuration) throws ConfigurationException
126    {
127        try
128        {
129            _validatorManager = new ThreadSafeComponentManager<>();
130            _validatorManager.setLogger(_logger);
131            _validatorManager.contextualize(_context);
132            _validatorManager.service(_manager);
133            
134            _enumeratorManager = new ThreadSafeComponentManager<>();
135            _enumeratorManager.setLogger(_logger);
136            _enumeratorManager.contextualize(_context);
137            _enumeratorManager.service(_manager);
138            
139            _fullPath = configuration.getChild("systemProperty").getAttribute("name");
140            int pos = _fullPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR);
141            
142            String systemPropertyId = pos > -1 ? _fullPath.substring(pos + ContentConstants.METADATA_PATH_SEPARATOR.length()) : _fullPath;
143            _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq"));
144            
145            if (!_systemPropEP.isSearchable(systemPropertyId))
146            {
147                throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not searchable.");
148            }
149            
150            String contentTypeId = configuration.getChild("contentTypes").getAttribute("baseId", null);
151            
152            _contentTypes = new HashSet<>();
153            String joinPath = pos > -1 ? _fullPath.substring(0, pos) : "";
154            _joinPaths = _configureJoinPaths(joinPath, contentTypeId, configuration);
155            _systemProperty = _systemPropEP.getExtension(systemPropertyId);
156            
157            setId(SEARCH_CRITERION_SYSTEM_PREFIX + _fullPath + "-" + _operator.getName());
158            setGroup(_configureI18nizableText(configuration.getChild("group", false), null));
159            
160            String validatorRole = "validator";
161            if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration))
162            {
163                validatorRole = null;
164            }
165            
166            setLabel(_systemProperty.getLabel());
167            setDescription(_systemProperty.getDescription());
168            MetadataType type = _systemProperty.getType();
169            setType(type);
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            Configuration enumeratorAndWidgetParamConf = _getEnumeratorAndWidgetParamConf(configuration);
176            EnumeratorDefinition enumDef = _systemProperty.getEnumeratorDefinition(enumeratorAndWidgetParamConf);
177            if (enumDef != null)
178            {
179                enumeratorRole = _initializeEnumerator(enumDef);
180            }
181            
182            setWidget(configureWidget(configuration, _systemProperty.getWidget(), type));
183            setWidgetParameters(configureWidgetParameters(configuration, _systemProperty.getWidgetParameters(enumeratorAndWidgetParamConf), type, _systemProperty.getContentTypeId()));
184            
185            // Potentially replace the standard label and description by the custom ones.
186            I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null);
187            if (userLabel != null)
188            {
189                setLabel(userLabel);
190            }
191            I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null);
192            if (userDescription != null)
193            {
194                setDescription(userDescription);
195            }
196            
197            configureUIProperties(configuration);
198            configureValues(configuration);
199            
200            if (enumeratorRole != null)
201            {
202                _enumeratorManager.initialize();
203                setEnumerator(_enumeratorManager.lookup(enumeratorRole));
204            }
205            
206            if (validatorRole != null)
207            {
208                _validatorManager.initialize();
209                setValidator(_validatorManager.lookup(validatorRole));
210            }
211        }
212        catch (Exception e)
213        {
214            throw new ConfigurationException("Error configuring the system search criterion.", configuration, e);
215        }
216    }
217    
218    private Configuration _getEnumeratorAndWidgetParamConf(Configuration critConf) throws ConfigurationException
219    {
220        if (isJoined())
221        {
222            DefaultConfiguration widgetParamConf = new DefaultConfiguration(critConf);
223            widgetParamConf.removeChild(widgetParamConf.getChild("contentTypes"));
224            if (!_contentTypes.isEmpty())
225            {
226                DefaultConfiguration cTypesConf = new DefaultConfiguration("contentTypes");
227                widgetParamConf.addChild(cTypesConf);
228                for (String contentType : _contentTypes)
229                {
230                    DefaultConfiguration cTypeConf = new DefaultConfiguration("type");
231                    cTypeConf.setAttribute("id", contentType);
232                    cTypesConf.addChild(cTypeConf);
233                }
234            }
235            
236            return widgetParamConf;
237        }
238        
239        // in case non join, the global conf already contains the real content types
240        return critConf;
241        
242    }
243    
244    @Override
245    public boolean isJoined()
246    {
247        return CollectionUtils.isNotEmpty(_joinPaths);
248    }
249    
250    @Override
251    public boolean isFacetable()
252    {
253        return _systemProperty.isFacetable();
254    }
255    
256    /**
257     * Get the operator.
258     * @return the operator.
259     */
260    public Operator getOperator()
261    {
262        return _operator;
263    }
264    
265    public String getFieldId()
266    {
267        return SEARCH_CRITERION_SYSTEM_PREFIX + _systemProperty.getId();
268    }
269    
270    /**
271     * Get id of this system property
272     * @return The system property's id
273     */
274    public String getSystemPropertyId()
275    {
276        return _systemProperty.getId();
277    }
278    
279    @Override
280    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
281    {
282        if (value == null || (value instanceof String && ((String) value).length() == 0) || (value instanceof List && ((List) value).isEmpty()))
283        {
284            return null;
285        }
286        
287        Operator operator = customOperator != null ? customOperator : getOperator();
288        
289        Query query = _systemProperty.getQuery(value, operator, language, contextualParameters);
290        
291        if (query != null && !_joinPaths.isEmpty())
292        {
293            query = new JoinQuery(query, _joinPaths);
294        }
295        
296        return query;
297    }
298    
299    @Override
300    public SearchField getSearchField()
301    {
302        SearchField sysSearchField = _systemProperty.getSearchField();
303        if (_joinPaths.isEmpty())
304        {
305            return sysSearchField;
306        }
307        else
308        {
309            return new JoinedSystemSearchField(_joinPaths, sysSearchField);
310        }
311    }
312    
313    /**
314     * Configure the join paths.
315     * @param joinPath The full join path.
316     * @param contentTypeId The base content type ID, can be null.
317     * @param configuration The configuration of the criterion
318     * @return The list of join paths.
319     * @throws ConfigurationException If the join paths are missconfigured
320     */
321    private List<String> _configureJoinPaths(String joinPath, String contentTypeId, Configuration configuration) throws ConfigurationException
322    {
323        List<String> joinPaths = new ArrayList<>();
324        
325        if (StringUtils.isNotBlank(joinPath))
326        {
327            if (StringUtils.isBlank(contentTypeId))
328            {
329                throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without a base content type.");
330            }
331            
332            ContentType cType = _cTypeEP.getExtension(contentTypeId);
333            IndexingModel indexingModel = cType.getIndexingModel();
334            
335            String[] pathSegments = StringUtils.split(joinPath, ContentConstants.METADATA_PATH_SEPARATOR);
336            String firstField = pathSegments[0];
337            IndexingField indexingField = indexingModel.getField(firstField);
338            
339            if (indexingField == null)
340            {
341                throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown indexing field: " + firstField);
342            }
343            
344            String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0];
345            
346            MetadataType type = _computeJoinPaths(indexingField, remainingPathSegments, joinPaths);
347            
348            // The final definition must be a content (to be able to extract a system property from it).
349            if (type != MetadataType.CONTENT)
350            {
351                throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content.");
352            }
353        }
354        else
355        {
356            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
357            {
358                _contentTypes.add(cTypeConf.getAttribute("id"));
359            }
360        }
361        
362        return joinPaths;
363    }
364    
365    /**
366     * Get the indexing field type and compute the join paths.
367     * @param indexingField The initial indexing field
368     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
369     * @param joinPaths The consecutive's path in case of joint to access the field/metadata
370     * @return The metadata definition or null if not found
371     * @throws ConfigurationException If an error occurs.
372     */
373    private MetadataType _computeJoinPaths(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException
374    {
375        if (indexingField instanceof MetadataIndexingField)
376        {
377            MetadataDefinition definition = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true);
378            // Add in _contentTypes the good types
379            Optional.ofNullable(definition.getContentType())
380                    .map(Collections::singletonList)
381                    .map(this::_typeAndSubTypes)
382                    .ifPresent(_contentTypes::addAll);
383            
384            return definition.getType();
385        }
386        else if (indexingField instanceof CustomIndexingField)
387        {
388            // Remaining path segments should be exactly 1 (the last path segment being the property).
389            if (remainingPathSegments.length != 1)
390            {
391                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));
392            }
393            else
394            {
395                // No more recursion
396                return indexingField.getType();
397            }
398        }
399        else
400        {
401            throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")");
402        }
403    }
404    
405    private Collection<String> _typeAndSubTypes(List<String> singletonType)
406    {
407        String typeInSingleton = singletonType.iterator().next();
408        return CollectionUtils.union(singletonType, _cTypeEP.getSubTypes(typeInSingleton));
409    }
410    
411    private String _initializeEnumerator(EnumeratorDefinition enumDef)
412    {
413        String role = null;
414        
415        if (enumDef.isStatic())
416        {
417            StaticEnumerator enumerator = new StaticEnumerator();
418            enumDef.getStaticEntries().entrySet().forEach(entry -> enumerator.add(entry.getValue(), entry.getKey()));
419            setEnumerator(enumerator);
420        }
421        else
422        {
423            role = "enumerator";
424            _enumeratorManager.addComponent("cms", null, role, enumDef.getEnumeratorClass(), enumDef.getConfiguration());
425        }
426        
427        return role;
428    }
429    
430}