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;
026import java.util.stream.Collectors;
027
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.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.commons.collections4.CollectionUtils;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.MetadataType;
038import org.ametys.cms.model.ContentElementDefinition;
039import org.ametys.cms.search.SearchField;
040import org.ametys.cms.search.content.ContentSearchHelper;
041import org.ametys.cms.search.model.SystemProperty;
042import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
043import org.ametys.cms.search.model.SystemSearchCriterion;
044import org.ametys.cms.search.query.JoinQuery;
045import org.ametys.cms.search.query.Query;
046import org.ametys.cms.search.query.Query.Operator;
047import org.ametys.cms.search.solr.field.JoinedSystemSearchField;
048import org.ametys.runtime.i18n.I18nizableText;
049import org.ametys.runtime.model.ModelHelper;
050import org.ametys.runtime.model.ModelItem;
051import org.ametys.runtime.model.exception.UndefinedItemPathException;
052import org.ametys.runtime.parameter.Validator;
053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
054
055/**
056 * This class is a search criteria on a system property (author, lastModified, with-comments, ...)
057 */
058public class SystemSearchUICriterion extends AbstractSearchUICriterion implements SystemSearchCriterion
059{
060    /** Prefix for id of system property search criteria */
061    public static final String SEARCH_CRITERION_SYSTEM_PREFIX = "property-";
062    
063    /** ComponentManager for {@link Validator}s. */
064    protected ThreadSafeComponentManager<Validator> _validatorManager;
065    
066    /** The system property extension point. */
067    protected SystemPropertyExtensionPoint _systemPropEP;
068    
069    /** The content search helper. */
070    protected ContentSearchHelper _searchHelper;
071    
072    /** The join paths */
073    protected List<String> _joinPaths;
074    
075    private Operator _operator;
076    private SystemProperty _systemProperty;
077    private Set<String> _contentTypes;
078    private String _fullPath;
079    
080    @Override
081    public void service(ServiceManager manager) throws ServiceException
082    {
083        super.service(manager);
084        _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
085        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
086    }
087    
088    @Override
089    public void dispose()
090    {
091        super.dispose();
092        _validatorManager.dispose();
093        _validatorManager = null;
094    }
095    
096    @Override
097    public void configure(Configuration configuration) throws ConfigurationException
098    {
099        try
100        {
101            _validatorManager = new ThreadSafeComponentManager<>();
102            _validatorManager.setLogger(_logger);
103            _validatorManager.contextualize(_context);
104            _validatorManager.service(_manager);
105            
106            _enumeratorManager = new ThreadSafeComponentManager<>();
107            _enumeratorManager.setLogger(_logger);
108            _enumeratorManager.contextualize(_context);
109            _enumeratorManager.service(_manager);
110            
111            _fullPath = configuration.getChild("systemProperty").getAttribute("name");
112            int pos = _fullPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR);
113            
114            String systemPropertyId = pos > -1 ? _fullPath.substring(pos + ModelItem.ITEM_PATH_SEPARATOR.length()) : _fullPath;
115            _operator = Operator.fromName(configuration.getChild("test-operator").getValue("eq"));
116            
117            if (!_systemPropEP.isSearchable(systemPropertyId))
118            {
119                throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not searchable.");
120            }
121            
122            Set<String> baseContentTypeIds = new HashSet<>();
123            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("baseType"))
124            {
125                baseContentTypeIds.add(cTypeConf.getAttribute("id"));
126            }
127            
128            _contentTypes = new HashSet<>();
129            String joinPath = pos > -1 ? _fullPath.substring(0, pos) : "";
130            _joinPaths = _configureJoinPaths(joinPath, baseContentTypeIds, configuration);
131            _systemProperty = _systemPropEP.getExtension(systemPropertyId);
132            
133            setId(SEARCH_CRITERION_SYSTEM_PREFIX + _fullPath + "-" + _operator.getName());
134            setGroup(_configureI18nizableText(configuration.getChild("group", false), null));
135            
136            String validatorRole = "validator";
137            if (!_initializeValidator(_validatorManager, "cms", validatorRole, configuration))
138            {
139                validatorRole = null;
140            }
141            
142            setLabel(_systemProperty.getLabel());
143            setDescription(_systemProperty.getDescription());
144            MetadataType type = MetadataType.fromModelItemType(_systemProperty.getType());
145            setType(type);
146            // Multiple defaults to false even for a multiple property.
147            setMultiple(configuration.getAttributeAsBoolean("multiple", false));
148            
149            // TODO Add the current criterion configuration.
150            Configuration enumeratorAndWidgetParamConf = _getEnumeratorAndWidgetParamConf(configuration);
151            setEnumerator(configureEnumerator(configuration, _systemProperty, enumeratorAndWidgetParamConf));
152            setWidget(configureWidget(configuration, _systemProperty));
153            setWidgetParameters(configureWidgetParameters(configuration, _systemProperty, enumeratorAndWidgetParamConf));
154            
155            // Potentially replace the standard label and description by the custom ones.
156            I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null);
157            if (userLabel != null)
158            {
159                setLabel(userLabel);
160            }
161            I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null);
162            if (userDescription != null)
163            {
164                setDescription(userDescription);
165            }
166            
167            configureUIProperties(configuration);
168            configureValues(configuration);
169            
170            if (validatorRole != null)
171            {
172                _validatorManager.initialize();
173                setValidator(_validatorManager.lookup(validatorRole));
174            }
175        }
176        catch (Exception e)
177        {
178            throw new ConfigurationException("Error configuring the system search criterion.", configuration, e);
179        }
180    }
181    
182    private Configuration _getEnumeratorAndWidgetParamConf(Configuration critConf) throws ConfigurationException
183    {
184        if (isJoined())
185        {
186            DefaultConfiguration widgetParamConf = new DefaultConfiguration(critConf);
187            widgetParamConf.removeChild(widgetParamConf.getChild("contentTypes"));
188            if (!_contentTypes.isEmpty())
189            {
190                DefaultConfiguration cTypesConf = new DefaultConfiguration("contentTypes");
191                widgetParamConf.addChild(cTypesConf);
192                for (String contentType : _contentTypes)
193                {
194                    DefaultConfiguration cTypeConf = new DefaultConfiguration("type");
195                    cTypeConf.setAttribute("id", contentType);
196                    cTypesConf.addChild(cTypeConf);
197                }
198            }
199            
200            return widgetParamConf;
201        }
202        
203        // in case non join, the global conf already contains the real content types
204        return critConf;
205        
206    }
207    
208    @Override
209    public boolean isJoined()
210    {
211        return CollectionUtils.isNotEmpty(_joinPaths);
212    }
213    
214    @Override
215    public boolean isFacetable()
216    {
217        return _systemProperty.isFacetable();
218    }
219    
220    /**
221     * Get the operator.
222     * @return the operator.
223     */
224    public Operator getOperator()
225    {
226        return _operator;
227    }
228    
229    public String getFieldId()
230    {
231        return SEARCH_CRITERION_SYSTEM_PREFIX + _systemProperty.getName();
232    }
233    
234    /**
235     * Get id of this system property
236     * @return The system property's id
237     */
238    public String getSystemPropertyId()
239    {
240        return _systemProperty.getName();
241    }
242    
243    @Override
244    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
245    {
246        if (customOperator != Operator.EXISTS
247            && (value == null
248                || value instanceof String && ((String) value).length() == 0
249                || value instanceof List && ((List) value).isEmpty()))
250        {
251            return null;
252        }
253        
254        Operator operator = customOperator != null ? customOperator : getOperator();
255        
256        Query query = _systemProperty.getQuery(value, operator, language, contextualParameters);
257        
258        if (query != null && !_joinPaths.isEmpty())
259        {
260            query = new JoinQuery(query, _joinPaths);
261        }
262        
263        return query;
264    }
265    
266    @Override
267    public SearchField getSearchField()
268    {
269        SearchField sysSearchField = _systemProperty.getSearchField();
270        if (_joinPaths.isEmpty())
271        {
272            return sysSearchField;
273        }
274        else
275        {
276            return new JoinedSystemSearchField(_joinPaths, sysSearchField);
277        }
278    }
279    
280    /**
281     * Configure the join paths.
282     * @param joinPath The full join path.
283     * @param contentTypeIds The base content type identifiers.
284     * @param configuration The configuration of the criterion
285     * @return The list of join paths.
286     * @throws ConfigurationException If the join paths are missconfigured
287     */
288    private List<String> _configureJoinPaths(String joinPath, Set<String> contentTypeIds, Configuration configuration) throws ConfigurationException
289    {
290        List<String> joinPaths = new ArrayList<>();
291        
292        if (StringUtils.isNotBlank(joinPath))
293        {
294            if (contentTypeIds.isEmpty())
295            {
296                throw new ConfigurationException("System search criterion with path '" + _fullPath + "': impossible to configure a joint system property without base content types.");
297            }
298            
299            try
300            {
301                Collection<ContentType> contentTypes = contentTypeIds.stream()
302                                                                     .map(_cTypeEP::getExtension)
303                                                                     .collect(Collectors.toList());
304                ModelItem modelItem = ModelHelper.getModelItem(joinPath, contentTypes);
305                // The definition must be a content (to be able to extract a system property from it).
306                if (modelItem instanceof ContentElementDefinition contentElementDefinition)
307                {
308                    joinPaths.addAll(_searchHelper.computeJoinPaths(joinPath, contentTypeIds, true));
309                    // Add in _contentTypes the good types
310                    Optional.ofNullable(contentElementDefinition.getContentTypeId())
311                            .map(Collections::singletonList)
312                            .map(this::_typeAndSubTypes)
313                            .ifPresent(_contentTypes::addAll);
314                }
315                else
316                {
317                    throw new ConfigurationException("'" + _fullPath + "' is not a valid system search criterion path as '" + joinPath + "' does not represent a content.");
318                }
319            }
320            catch (UndefinedItemPathException e)
321            {
322                throw new ConfigurationException("System search criterion with path '" + _fullPath + "' refers to an unknown model item: " + joinPath, e);
323            }
324        }
325        else
326        {
327            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
328            {
329                _contentTypes.add(cTypeConf.getAttribute("id"));
330            }
331        }
332        
333        return joinPaths;
334    }
335    
336    private Collection<String> _typeAndSubTypes(List<String> singletonType)
337    {
338        String typeInSingleton = singletonType.iterator().next();
339        return CollectionUtils.union(singletonType, _cTypeEP.getSubTypes(typeInSingleton));
340    }
341}