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