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.ui.model.impl;
017
018import java.util.ArrayList;
019import java.util.LinkedHashMap;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Disposable;
028import org.apache.avalon.framework.component.ComponentException;
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.configuration.MutableConfiguration;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.commons.lang3.tuple.ImmutablePair;
039import org.apache.commons.lang3.tuple.Pair;
040import org.slf4j.Logger;
041
042import org.ametys.cms.contenttype.ContentTypesHelper;
043import org.ametys.cms.contenttype.MetadataType;
044import org.ametys.cms.search.query.AndQuery;
045import org.ametys.cms.search.query.OrQuery;
046import org.ametys.cms.search.query.Query;
047import org.ametys.cms.search.query.Query.Operator;
048import org.ametys.cms.search.ui.model.SearchUICriterion;
049import org.ametys.runtime.plugin.component.LogEnabled;
050import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
051
052/**
053 * Aggregate multiple indexing field as a unique search criteria.
054 * The resulting query is an OR query on all the terms.
055 */
056public class IndexingFieldAggregatorSearchUICriterion extends AbstractSearchUICriterion implements Contextualizable, Disposable, LogEnabled
057{
058    /** Prefix for id of indexing field aggregator search criteria */
059    public static final String SEARCH_CRITERIA_METADATA_AGGREGATOR_PREFIX = "metadata-aggregator-";
060    
061    /** ComponentManager for aggregated {@link SearchUICriterion}s. */
062    protected ThreadSafeComponentManager<SearchUICriterion> _fieldCriteriaManager;
063    
064    /** The map of aggregated criteria */
065    protected Map<String, SearchUICriterion> _fieldCriteriaMap;
066    
067    /** The logger */
068    protected Logger _logger;
069    
070    /** The service manager */
071    protected ServiceManager _manager;
072
073    /** The helper for convenient methods on content types */
074    protected ContentTypesHelper _contentTypesHelper;
075    
076    /** The context. */
077    protected Context _context;
078
079    /** The type of the aggregated fields */
080    protected MetadataType _type;
081
082    @Override
083    public void setLogger(Logger logger)
084    {
085        _logger = logger;
086    }
087    
088    @Override
089    public void contextualize(Context context) throws ContextException
090    {
091        _context = context;
092    }
093    
094    @Override
095    public void service(ServiceManager manager) throws ServiceException
096    {
097        super.service(manager);
098        _manager = manager;
099        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
100    }
101    
102    @Override
103    public void configure(Configuration configuration) throws ConfigurationException
104    {
105        try
106        {
107            super.configure(configuration);
108            
109            _fieldCriteriaManager = new ThreadSafeComponentManager<>();
110            _fieldCriteriaManager.setLogger(_logger);
111            _fieldCriteriaManager.contextualize(_context);
112            _fieldCriteriaManager.service(_manager);
113            
114            MutableConfiguration baseConf = new DefaultConfiguration(configuration);
115            baseConf.removeChild(baseConf.getChild("field"));
116            
117            Configuration fieldsConf = configuration.getChild("fields");
118            Configuration[] fieldConfs = fieldsConf.getChildren("field");
119            
120            List<Pair<String, Configuration>> fieldSearchUICriterionToLookup = _fieldSearchUICriterionToLookup(fieldConfs, baseConf);
121            
122            _initializeFieldCriteria(fieldSearchUICriterionToLookup, fieldsConf);
123            
124            _configureWidgetAndWidgetParameters(configuration);
125        }
126        catch (ConfigurationException e)
127        {
128            throw e;
129        }
130        catch (Exception e)
131        {
132            throw new ConfigurationException("Unable to create local component manager.", configuration, e);
133        }
134    }
135    
136    private List<Pair<String, Configuration>>  _fieldSearchUICriterionToLookup(Configuration[] fieldConfs, MutableConfiguration baseConf) throws ConfigurationException
137    {
138        List<Pair<String, Configuration>> fieldSearchUICriterionToLookup = new ArrayList<>();
139        
140        for (Configuration fieldConf : fieldConfs)
141        {
142            String path = fieldConf.getAttribute("path");
143            
144            MutableConfiguration indexingFieldConf = new DefaultConfiguration(baseConf);
145            DefaultConfiguration newFieldConf = new DefaultConfiguration(fieldConf);
146            indexingFieldConf.addChild(newFieldConf);
147            newFieldConf.setAttribute("path", path);
148            
149            String role = path;
150            _fieldCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, indexingFieldConf);
151            
152            fieldSearchUICriterionToLookup.add(ImmutablePair.of(role, fieldConf));
153        }
154        
155        return fieldSearchUICriterionToLookup;
156    }
157    
158    private void _initializeFieldCriteria(List<Pair<String, Configuration>> fieldSearchUICriterionToLookup, Configuration fieldsConf) throws Exception
159    {
160        String typeAttr = fieldsConf.getAttribute("type", null);
161        
162        _fieldCriteriaManager.initialize();
163        
164        _fieldCriteriaMap = new LinkedHashMap<>();
165        _type = Optional.ofNullable(typeAttr)
166                        .map(String::toUpperCase)
167                        .map(MetadataType::valueOf)
168                        .orElse(null);
169        for (Pair<String, Configuration> pair : fieldSearchUICriterionToLookup)
170        {
171            String role = pair.getLeft();
172            SearchUICriterion criterion;
173            try
174            {
175                criterion = _fieldCriteriaManager.lookup(role);
176            }
177            catch (ComponentException e)
178            {
179                throw new ConfigurationException("Impossible to lookup the search criterion of role: " + role, e);
180            }
181            
182            MetadataType criterionType = criterion.getType();
183            if (_type == null)
184            {
185                _type = criterionType;
186            }
187            else if (_type != criterionType)
188            {
189                String location = pair.getRight().getLocation();
190                throw new ConfigurationException("Search criterion of role '" + role + "' is of type '" + criterionType + "' but should be of type '" + _type + "' at " + location + ".");
191            }
192            _fieldCriteriaMap.put(criterion.getId(), criterion);
193        }
194    }
195    
196    private void _configureWidgetAndWidgetParameters(Configuration configuration) throws ConfigurationException
197    {
198        setWidget(configureWidget(configuration, null, _type));
199        
200        String commonContentTypeId = null; 
201        if (_type == MetadataType.CONTENT || _type == MetadataType.SUB_CONTENT)
202        {
203            Set<String> contentTypeIds = _fieldCriteriaMap.values().stream()
204                                                          .map(SearchUICriterion::getContentTypeId)
205                                                          .collect(Collectors.toSet());
206            Set<String> commonContentTypeIds = _contentTypesHelper.getCommonAncestors(contentTypeIds);
207            
208            if (commonContentTypeIds.size() == 1)
209            {
210                commonContentTypeId = commonContentTypeIds.iterator().next();
211            }
212        }
213        setWidgetParameters(configureWidgetParameters(configuration, null, _type, commonContentTypeId));
214    }
215    
216    @Override
217    protected void configureId(Configuration configuration) throws ConfigurationException
218    {
219        String customRef = configuration.getAttribute("custom-ref");
220        setId(SEARCH_CRITERIA_METADATA_AGGREGATOR_PREFIX + customRef);
221    }
222    
223    @Override
224    public MetadataType getType()
225    {
226        return _type;
227    }
228    
229    @Override
230    public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters)
231    {
232        List<Query> fieldQueries = new LinkedList<>();
233        
234        for (String role : _fieldCriteriaMap.keySet())
235        {
236            SearchUICriterion searchCriteria = _fieldCriteriaMap.get(role);
237            Query query = searchCriteria.getQuery(value, customOperator, allValues, language, contextualParameters);
238            
239            if (query != null)
240            {
241                fieldQueries.add(query);
242            }
243        }
244        
245        if (fieldQueries.isEmpty())
246        {
247            return null;
248        }
249        else
250        {
251            return (customOperator != null && Operator.NE.equals(customOperator)) ? new AndQuery(fieldQueries) : new OrQuery(fieldQueries);
252        }
253    }
254    
255    public void dispose()
256    {
257        _fieldCriteriaManager.dispose();
258        _fieldCriteriaManager = null;
259    }
260    
261    @Override
262    public String getFieldId()
263    {
264        return getId();
265    }
266    
267    @Override
268    public boolean isSortable()
269    {
270        // Not destined to be displayed.
271        return false;
272    }
273    
274    @Override
275    public Operator getOperator()
276    {
277        return null;
278    }
279    
280}