001/*
002 *  Copyright 2014 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.model.impl;
017
018import java.util.ArrayList;
019import java.util.HashSet;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.component.ComponentException;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfiguration;
029import org.apache.avalon.framework.configuration.MutableConfiguration;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.commons.lang3.StringUtils;
036
037import org.ametys.cms.contenttype.ContentType;
038import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
039import org.ametys.cms.contenttype.ContentTypesHelper;
040import org.ametys.cms.model.CMSDataContext;
041import org.ametys.cms.search.model.SearchModelCriterionDefinitionHelper;
042import org.ametys.cms.search.model.SearchModelCriterionDefinition;
043import org.ametys.cms.search.query.AndQuery;
044import org.ametys.cms.search.query.OrQuery;
045import org.ametys.cms.search.query.Query;
046import org.ametys.cms.search.query.Query.Operator;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.model.ItemParserHelper;
049import org.ametys.runtime.model.Model;
050import org.ametys.runtime.model.exception.UnknownTypeException;
051import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
052
053/**
054 * Aggregate multiple referencing criterion definitions as a unique criterion definition.
055 * The resulting query is an OR query on all the terms.
056 * @param <T> Type of the criteria values
057 */
058public class ReferencingAggregatorCriterionDefinition<T> extends AbstractStaticSearchModelCriterionDefinition<T> implements Contextualizable
059{
060    /** Prefix for name of referencing aggregator criterion definition */
061    public static final String CRITERION_DEFINITION_AGGREGATOR_PREFIX = "reference-aggregator-";
062    
063    /** The service manager */
064    protected ServiceManager _manager;
065    
066    /** The avalon context */
067    protected Context _context;
068    
069    /** The content type extension point */
070    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
071    
072    /** The helper for convenient methods on content types */
073    protected ContentTypesHelper _contentTypesHelper;
074    
075    /** The helper for search model criterion definition */
076    protected SearchModelCriterionDefinitionHelper _searchModelCriterionDefinitionHelper;
077    
078    /** The aggregated criteria */
079    protected List<SearchModelCriterionDefinition<T>> _criteria;
080    
081    /** the criterion type identifier */
082    protected String _typeId;
083    
084    public void contextualize(Context context) throws ContextException
085    {
086        _context = context;
087    }
088    
089    @Override
090    public void service(ServiceManager manager) throws ServiceException
091    {
092        super.service(manager);
093        
094        _manager = manager;
095        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
096        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
097        _searchModelCriterionDefinitionHelper = (SearchModelCriterionDefinitionHelper) manager.lookup(SearchModelCriterionDefinitionHelper.ROLE);
098    }
099    
100    @Override
101    public void configure(Configuration configuration) throws ConfigurationException
102    {
103        super.configure(configuration);
104        
105        try
106        {
107            setCriteria(_parseCriteria(configuration));
108            
109            _setTypeId(_parseTypeId(configuration));
110            setWidget(ItemParserHelper.parseWidget(configuration));
111            setWidgetParameters(ItemParserHelper.parseWidgetParameters(configuration, getPluginName()));
112        }
113        catch (ConfigurationException e)
114        {
115            throw e;
116        }
117        catch (Exception e)
118        {
119            throw new ConfigurationException("Unable to create local component manager.", configuration, e);
120        }
121    }
122    
123    private List<SearchModelCriterionDefinition<T>> _parseCriteria(Configuration configuration) throws ConfigurationException, Exception
124    {
125        ThreadSafeComponentManager<SearchModelCriterionDefinition> criteriaManager = new ThreadSafeComponentManager<>();
126        criteriaManager.setLogger(_logger);
127        criteriaManager.contextualize(_context);
128        criteriaManager.service(_manager);
129        
130        try
131        {
132            List<String>  criteriaToLookup = _getCriteriaToLookup(configuration, criteriaManager);
133            criteriaManager.initialize();
134            return _initializeCriteria(criteriaToLookup, criteriaManager);
135        }
136        finally
137        {
138            criteriaManager.dispose();
139            criteriaManager = null;
140        }
141    }
142        
143    private List<String> _getCriteriaToLookup(Configuration configuration, ThreadSafeComponentManager<SearchModelCriterionDefinition> criteriaManager) throws ConfigurationException
144    {
145        List<String> criteriaToLookup = new ArrayList<>();
146        
147        MutableConfiguration baseAggregatorConfiguration = new DefaultConfiguration(configuration);
148        baseAggregatorConfiguration.removeChild(baseAggregatorConfiguration.getChild("items"));
149
150        Configuration[] criteriaConfigurations = configuration.getChild("items")
151                                                              .getChildren("item");
152        
153        Set<ContentType> contentTypes = new HashSet<>();
154        for (Configuration contentTypeConf : configuration.getChild("contentTypes").getChildren("type"))
155        {
156            String contentTypeId = contentTypeConf.getAttribute("id");
157            if (_contentTypeExtensionPoint.hasExtension(contentTypeId))
158            {
159                contentTypes.add(_contentTypeExtensionPoint.getExtension(contentTypeId));
160            }
161            else
162            {
163                throw new ConfigurationException("Unable to configure the criterion definition '" + getName() + "'. The referenced content type '" + contentTypeId + "' does not exist", configuration);
164            }
165        }
166        
167        for (Configuration crinterionConfiguration : criteriaConfigurations)
168        {
169            String referencePath = crinterionConfiguration.getAttribute("ref");
170            MutableConfiguration crinterionConfigurationWithBase = new DefaultConfiguration(baseAggregatorConfiguration);
171            Configuration crinterionConfigurationCopy = new DefaultConfiguration(crinterionConfiguration);
172            crinterionConfigurationWithBase.addChild(crinterionConfigurationCopy);
173            
174            Class<? extends SearchModelCriterionDefinition> criterionDefinitionClass = _searchModelCriterionDefinitionHelper.getStaticCriterionDefinitionClass(contentTypes, referencePath);
175            if (criterionDefinitionClass != null)
176            {
177                criteriaManager.addComponent("cms", null, referencePath, criterionDefinitionClass, crinterionConfigurationWithBase);
178                criteriaToLookup.add(referencePath);
179            }
180        }
181        
182        return criteriaToLookup;
183    }
184        
185    @SuppressWarnings("unchecked")
186    private List<SearchModelCriterionDefinition<T>> _initializeCriteria(List<String> criteriaToLookup, ThreadSafeComponentManager<SearchModelCriterionDefinition> criteriaManager) throws Exception
187    {
188        List<SearchModelCriterionDefinition<T>> criteria = new ArrayList<>();
189        for (String role: criteriaToLookup)
190        {
191            try
192            {
193                SearchModelCriterionDefinition criterion = criteriaManager.lookup(role);
194                criteria.add(criterion);
195            }
196            catch (ComponentException e)
197            {
198                throw new ConfigurationException("Impossible to lookup the criterion definition of role: " + role, e);
199            }
200        }
201        
202        return criteria;
203    }
204    
205    private String _parseTypeId(Configuration configuration) throws ConfigurationException
206    {
207        String typeId = configuration.getChild("items")
208                                     .getAttribute("type", null);
209        
210        if (typeId != null && !_criterionTypeExtensionPoint.hasExtension(typeId))
211        {
212            String availableTypes = StringUtils.join(_criterionTypeExtensionPoint.getExtensionsIds(), ", ");
213            UnknownTypeException ute = new UnknownTypeException("The type '" + typeId + "' is not available for the extension point '" + _criterionTypeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
214            throw new ConfigurationException("Unable to find the type '" + typeId + "' defined on the item '" + getName() + "'.", ute);
215        }
216        else
217        {
218            // Check type of all references
219            for (SearchModelCriterionDefinition criterion : getCriteria())
220            {
221                String criterionTypeId = criterion.getType().getId();
222                if (typeId == null)
223                {
224                    typeId = criterionTypeId;
225                }
226                else if (!typeId.equals(criterionTypeId))
227                {
228                    throw new ConfigurationException("Search criterion '" + criterion.getName() + "' is of type '" + criterionTypeId + "' but should be of type '" + typeId + ".", configuration);
229                }
230            }
231            
232            return typeId;
233        }
234    }
235    
236    @Override
237    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
238    {
239        List<Query> fieldQueries = new LinkedList<>();
240        
241        for (SearchModelCriterionDefinition<T> criterion : getCriteria())
242        {
243            Query query = criterion.getQuery(value, customOperator, allValues, language, contextualParameters);
244            
245            if (query != null)
246            {
247                fieldQueries.add(query);
248            }
249        }
250        
251        if (fieldQueries.isEmpty())
252        {
253            return null;
254        }
255        else
256        {
257            return (customOperator != null && Operator.NE.equals(customOperator)) ? new AndQuery(fieldQueries) : new OrQuery(fieldQueries);
258        }
259    }
260    
261    @Override
262    public String getName()
263    {
264        return CRITERION_DEFINITION_AGGREGATOR_PREFIX + super.getName();
265    }
266    
267    /**
268     * Retrieves the aggregated criteria
269     * @return the aggregated criteria
270     */
271    protected List<SearchModelCriterionDefinition<T>> getCriteria()
272    {
273        return _criteria;
274    }
275    
276    /**
277     * Set the criteria to aggregate.
278     * @param criteria the criteria to aggregate.
279     */
280    protected void setCriteria(List<SearchModelCriterionDefinition<T>> criteria)
281    {
282        _criteria = criteria;
283    }
284    
285    @Override
286    public void setModel(Model model)
287    {
288        super.setModel(model);
289        
290        for (SearchModelCriterionDefinition<T> criterion : getCriteria())
291        {
292            criterion.setModel(model);
293        }
294    }
295    
296    @Override
297    protected Map<String, I18nizableText> _getDefaultWidgetParameters()
298    {
299        List<SearchModelCriterionDefinition<T>> criteria = getCriteria();
300        if (!criteria.isEmpty())
301        {
302            // Use first criterion in context to have a ContentElementDefinition if needed
303            return getType().getDefaultCriterionWidgetParameters(CMSDataContext.newInstance()
304                            .withModelItem(criteria.get(0)));
305        }
306        else
307        {
308            return super._getDefaultWidgetParameters();
309        }
310    }
311    
312    @Override
313    public Operator getOperator()
314    {
315        return null;
316    }
317    
318    /**
319     * Set the criterion's type identifier
320     * @param typeId the type identifier to set
321     */
322    protected void _setTypeId(String typeId)
323    {
324        _typeId = typeId;
325    }
326    
327    @Override
328    protected String getTypeId()
329    {
330        return _typeId;
331    }
332}