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 && (value == null || (value instanceof String && ((String) value).length() == 0) || (value instanceof List && ((List) value).isEmpty())))
289        {
290            return null;
291        }
292        
293        Operator operator = customOperator != null ? customOperator : getOperator();
294        
295        Query query = _systemProperty.getQuery(value, operator, language, contextualParameters);
296        
297        if (query != null && !_joinPaths.isEmpty())
298        {
299            query = new JoinQuery(query, _joinPaths);
300        }
301        
302        return query;
303    }
304    
305    @Override
306    public SearchField getSearchField()
307    {
308        SearchField sysSearchField = _systemProperty.getSearchField();
309        if (_joinPaths.isEmpty())
310        {
311            return sysSearchField;
312        }
313        else
314        {
315            return new JoinedSystemSearchField(_joinPaths, sysSearchField);
316        }
317    }
318    
319    /**
320     * Configure the join paths.
321     * @param joinPath The full join path.
322     * @param contentTypeIds The base content type identifiers.
323     * @param configuration The configuration of the criterion
324     * @return The list of join paths.
325     * @throws ConfigurationException If the join paths are missconfigured
326     */
327    private List<String> _configureJoinPaths(String joinPath, Set<String> contentTypeIds, Configuration configuration) throws ConfigurationException
328    {
329        List<String> joinPaths = new ArrayList<>();
330        
331        if (StringUtils.isNotBlank(joinPath))
332        {
333            if (contentTypeIds.isEmpty())
334            {
335                throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without base content types.");
336            }
337            
338            Iterator<IndexingModel> indexingModels = contentTypeIds.stream()
339                    .map(_cTypeEP::getExtension)
340                    .map(ContentType::getIndexingModel)
341                    .iterator();
342            
343            String[] pathSegments = StringUtils.split(joinPath, ModelItem.ITEM_PATH_SEPARATOR);
344            String firstField = pathSegments[0];
345            
346            IndexingField indexingField = null;
347            while (indexingModels.hasNext() && indexingField == null)
348            {
349                indexingField = indexingModels.next().getField(firstField);
350            }
351            
352            if (indexingField == null)
353            {
354                throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown indexing field: " + firstField);
355            }
356            
357            String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0];
358            
359            MetadataType type = _computeJoinPaths(indexingField, remainingPathSegments, joinPaths);
360            
361            // The final definition must be a content (to be able to extract a system property from it).
362            if (type != MetadataType.CONTENT)
363            {
364                throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content.");
365            }
366        }
367        else
368        {
369            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
370            {
371                _contentTypes.add(cTypeConf.getAttribute("id"));
372            }
373        }
374        
375        return joinPaths;
376    }
377    
378    /**
379     * Get the indexing field type and compute the join paths.
380     * @param indexingField The initial indexing field
381     * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field
382     * @param joinPaths The consecutive's path in case of joint to access the field/metadata
383     * @return The metadata definition or null if not found
384     * @throws ConfigurationException If an error occurs.
385     */
386    private MetadataType _computeJoinPaths(IndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths) throws ConfigurationException
387    {
388        if (indexingField instanceof MetadataIndexingField)
389        {
390            MetadataDefinition definition = getMetadataDefinition((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true);
391            // Add in _contentTypes the good types
392            Optional.ofNullable(definition.getContentType())
393                    .map(Collections::singletonList)
394                    .map(this::_typeAndSubTypes)
395                    .ifPresent(_contentTypes::addAll);
396            
397            return definition.getType();
398        }
399        else if (indexingField instanceof CustomIndexingField)
400        {
401            // Remaining path segments should be exactly 1 (the last path segment being the property).
402            if (remainingPathSegments.length != 1)
403            {
404                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));
405            }
406            else
407            {
408                // No more recursion
409                return indexingField.getType();
410            }
411        }
412        else
413        {
414            throw new ConfigurationException("Unsupported class of indexing field:" + indexingField.getName() + " (" + indexingField.getClass().getName() + ")");
415        }
416    }
417    
418    private Collection<String> _typeAndSubTypes(List<String> singletonType)
419    {
420        String typeInSingleton = singletonType.iterator().next();
421        return CollectionUtils.union(singletonType, _cTypeEP.getSubTypes(typeInSingleton));
422    }
423    
424    private String _initializeEnumerator(EnumeratorDefinition enumDef)
425    {
426        String role = null;
427        
428        if (enumDef.isStatic())
429        {
430            StaticEnumerator enumerator = new StaticEnumerator();
431            enumDef.getStaticEntries().entrySet().forEach(entry -> enumerator.add(entry.getValue(), entry.getKey()));
432            setEnumerator(enumerator);
433        }
434        else
435        {
436            role = "enumerator";
437            _enumeratorManager.addComponent("cms", null, role, enumDef.getEnumeratorClass(), enumDef.getConfiguration());
438        }
439        
440        return role;
441    }
442    
443}