001/*
002 *  Copyright 2016 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.solr;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027import org.apache.avalon.framework.configuration.Configuration;
028import org.apache.avalon.framework.configuration.ConfigurationException;
029import org.apache.avalon.framework.context.Context;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.lang3.StringUtils;
033import org.slf4j.Logger;
034
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.search.model.SearchCriterion;
038import org.ametys.cms.search.model.SearchCriterionHelper;
039import org.ametys.cms.search.model.SearchModel;
040import org.ametys.cms.search.model.SystemProperty;
041import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
042import org.ametys.cms.search.query.Query.Operator;
043import org.ametys.cms.search.ui.model.ColumnHelper;
044import org.ametys.cms.search.ui.model.ColumnHelper.Column;
045import org.ametys.cms.search.ui.model.SearchUICriterion;
046import org.ametys.cms.search.ui.model.SearchUIModel;
047import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
048import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
049import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
050import org.ametys.runtime.model.ModelViewItem;
051import org.ametys.runtime.model.ViewItemContainer;
052import org.ametys.runtime.plugin.component.LogEnabled;
053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
054
055/**
056 * Search model wrapper which handles custom on-the-fly columns and facets.
057 */
058public class CriteriaSearchModelWrapper implements SearchModel, LogEnabled
059{
060    /** ComponentManager for {@link SearchUICriterion}s. */
061    protected ThreadSafeComponentManager<SearchCriterion> _searchCriterionManager;
062    
063    /** The logger. */
064    protected Logger _logger;
065    
066    private ServiceManager _serviceManager;
067    
068    private ContentTypeExtensionPoint _contentTypeExtensionPoint;
069    private SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
070    private SearchUIModelExtensionPoint _searchUIModelExtensionPoint;
071    private SearchCriterionHelper _searchCriterionHelper;
072    private ColumnHelper _columnHelper;
073    
074    private SearchModel _wrappedModel;
075    
076    /** The search criteria used as facets, indexed by ID. */
077    private Map<String, SearchCriterion> _facetedCriteria;
078    /** The result items */
079    private ViewItemContainer _resultItems;
080    
081    private int _criteriaIndex;
082    
083    /**
084     * Build a model wrapper.
085     * @param model the search model to wrap.
086     * @param manager the service manager.
087     * @param context the component context.
088     * @param logger the logger.
089     */
090    public CriteriaSearchModelWrapper(SearchModel model, ServiceManager manager, Context context, Logger logger)
091    {
092        _wrappedModel = model;
093
094        _serviceManager = manager;
095        _logger = logger;
096        
097        try
098        {
099            _searchCriterionManager = new ThreadSafeComponentManager<>();
100            _searchCriterionManager.setLogger(logger);
101            _searchCriterionManager.contextualize(context);
102            _searchCriterionManager.service(manager);
103        }
104        catch (Exception e)
105        {
106            _logger.error("Error initializing the SearchModel", e);
107        }
108    }
109    
110    public void setLogger(final Logger logger)
111    {
112        _logger = logger;
113    }
114    
115    /**
116     * Get the logger.
117     * @return the logger.
118     */
119    protected final Logger getLogger()
120    {
121        return _logger;
122    }
123    
124    /**
125     * Set the custom faceted criteria.
126     * @param baseContentTypeIds the reference content type identifiers.
127     * @param criteriaIds the criteria IDs
128     * @param contextualParameters the contextual parameters
129     * @return the valid criteria IDs
130     * @throws Exception if an error occurs initializing criteria.
131     */
132    public Collection<String> setFacetedCriteria(Set<String> baseContentTypeIds, Collection<String> criteriaIds, Map<String, Object> contextualParameters) throws Exception
133    {
134        Collection<String> resultCriteriaIds = null;
135        
136        if (criteriaIds != null)
137        {
138            _facetedCriteria = new LinkedHashMap<>(criteriaIds.size());
139            
140            List<Object> searchToolCriterionRoles = new ArrayList<>();
141            
142            resultCriteriaIds = configureFacets(searchToolCriterionRoles, baseContentTypeIds, criteriaIds, _wrappedModel, contextualParameters);
143            
144            _searchCriterionManager.initialize();
145            
146            for (Object critObj : searchToolCriterionRoles)
147            {
148                SearchCriterion criterion = null;
149                if (critObj instanceof SearchCriterion)
150                {
151                    // Already existing SearchCriterion object (taken from the wrapped model).
152                    criterion = (SearchCriterion) critObj;
153                }
154                else if (critObj instanceof String)
155                {
156                    // Criterion just added in the local component manager, we have to look it up.
157                    criterion = _searchCriterionManager.lookup((String) critObj);
158                }
159                
160                if (criterion != null && criterion.isFacetable())
161                {
162                    _facetedCriteria.put(criterion.getId(), criterion);
163                }
164            }
165        }
166        
167        return resultCriteriaIds != null ? resultCriteriaIds : Collections.emptySet();
168    }
169    
170    /**
171     * Set the custom columns.
172     * @param baseContentTypeIds the reference content type identifiers.
173     * @param columns The columns
174     * @param contextualParameters the contextual parameters
175     * @throws Exception if an error occurs initializing columns.
176     */
177    public void setResultColumns(Set<String> baseContentTypeIds, Collection<Column> columns, Map<String, Object> contextualParameters) throws Exception
178    {
179        String wrappedModelId = (String) contextualParameters.get("wrappedModelId");
180        if (StringUtils.isNotEmpty(wrappedModelId))
181        {
182            // Dashboard
183            SearchUIModel model = _getSearchUIModelExtensionPoint().getExtension(wrappedModelId);
184            _resultItems = model.getResultItems(contextualParameters);
185        }
186        else if (columns != null)
187        {
188            _resultItems = _getColumnHelper().createViewFromColumns(baseContentTypeIds, columns, false);
189        }
190    }
191    
192    /**
193     * Configure the list of faceted criteria.
194     * @param criteriaRoles the roles of criteria to lookup (or the already existing SearchUICriterion objects).
195     * @param baseContentTypeIds The reference content type identifiers.
196     * @param criteriaIds the criteria IDs.
197     * @param referenceModel the reference model.
198     * @param contextualParameters the contextual parameters
199     * @return the valid criteria IDs
200     * @throws ConfigurationException if an error occurs creating a component configuration.
201     */
202    protected Collection<String> configureFacets(List<Object> criteriaRoles, Set<String> baseContentTypeIds, Collection<String> criteriaIds, SearchModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException
203    {
204        Collection<String> resultCriteriaIds = new ArrayList<>();
205        
206        for (String criterionId : criteriaIds)
207        {
208            String[] facetPathSegments = StringUtils.split(criterionId, '/');
209            String lastSegmentOfFacetPath = facetPathSegments[facetPathSegments.length - 1];
210            
211            SearchCriterion referenceCriterion = null;
212            if (referenceModel != null)
213            {
214                referenceCriterion = getCriterion(referenceModel, criterionId, contextualParameters);
215            }
216            
217            if (referenceCriterion != null)
218            {
219                criteriaRoles.add(referenceCriterion);
220            }
221            else if (_getSystemPropertyExtensionPoint().hasExtension(lastSegmentOfFacetPath))
222            {
223                SystemProperty systemProperty = _getSystemPropertyExtensionPoint().getExtension(lastSegmentOfFacetPath);
224                if (systemProperty.isFacetable())
225                {
226                    addSystemCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId);
227                }
228                else
229                {
230                    getLogger().warn("The declared facet '{}' is a system property but is not facetable. Thus, it will not be added to the facets.", criterionId);
231                    break;
232                }
233            }
234            else
235            {
236                if (!baseContentTypeIds.isEmpty())
237                {
238                    // Metadata property.
239                    addIndexingFieldCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId);
240                }
241                else if (Content.ATTRIBUTE_TITLE.equals(criterionId))
242                {
243                    // title property of a random ContentType.
244                    String firstCTypeId = _getContentTypeExtensionPoint().getExtensionsIds().iterator().next();
245                    addIndexingFieldCriteriaComponents(criteriaRoles, Set.of(firstCTypeId), criterionId);
246                }
247                else
248                {
249                    break;
250                }
251            }
252            
253            resultCriteriaIds.add(criterionId);
254        }
255        
256        return resultCriteriaIds;
257    }
258    
259    /**
260     * Search a criterion in the reference model from its criterion identifier.
261     * @param searchModel the reference search model.
262     * @param criterionId the criterion identifier.
263     * @param contextualParameters the contextual parameters
264     * @return the criterion if found, null otherwise.
265     */
266    protected SearchCriterion getCriterion(SearchModel searchModel, String criterionId, Map<String, Object> contextualParameters)
267    {
268        Map<String, ? extends SearchCriterion> criteria = searchModel.getFacetedCriteria(contextualParameters);
269        
270        for (SearchCriterion criterion : criteria.values())
271        {
272            if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId))
273            {
274                return criterion;
275            }
276            else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId))
277            {
278                return criterion;
279            }
280            else if (criterion.getId().equals(criterionId))
281            {
282                return criterion;
283            }
284        }
285        
286        return null;
287    }
288    
289
290    /**
291     * Add a indexing field criteria component to the manager.
292     * @param searchToolCriterionRoles the criteria role list to fill.
293     * @param baseContentTypeIds the reference content type identifiers.
294     * @param fieldRef the field path.
295     * @throws ConfigurationException if an error occurs.
296     */
297    protected void addIndexingFieldCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String fieldRef) throws ConfigurationException
298    {
299        try
300        {
301            String slashPath = fieldRef.replace('.', '/');
302            
303            String role = fieldRef + _criteriaIndex;
304            _criteriaIndex++;
305            Configuration criteriaConf = _getSearchCriterionHelper().getIndexingFieldCriteriaConfiguration(this, Optional.empty(), baseContentTypeIds, slashPath, Optional.of(Operator.EQ), Optional.empty());
306            
307            _searchCriterionManager.addComponent("search", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
308            
309            searchToolCriterionRoles.add(role);
310        }
311        catch (Exception e)
312        {
313            throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, e);
314        }        
315    }
316    
317    /**
318     * Add a system criteria component to the manager.
319     * @param searchToolCriterionRoles the criteria role list to fill.
320     * @param baseContentTypeIds the reference content type identifiers.
321     * @param property the system property id.
322     * @throws ConfigurationException if an error occurs.
323     */
324    protected void addSystemCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String property) throws ConfigurationException
325    {
326        try
327        {
328            String role = property + _criteriaIndex;
329            _criteriaIndex++;
330            
331            Configuration criteriaConf = _getSearchCriterionHelper().getSystemCriteriaConfiguration(this, Optional.empty(), baseContentTypeIds, property, Optional.empty());
332            _searchCriterionManager.addComponent("search", null, role, SystemSearchUICriterion.class, criteriaConf);
333            
334            searchToolCriterionRoles.add(role);
335        }
336        catch (Exception e)
337        {
338            throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, e);
339        }
340    }
341    
342    @Override
343    public Map<String, ? extends SearchCriterion> getFacetedCriteria(Map<String, Object> contextualParameters)
344    {
345        if (_facetedCriteria != null && !_facetedCriteria.isEmpty())
346        {
347            return Collections.unmodifiableMap(_facetedCriteria);
348        }
349        else
350        {
351            return _wrappedModel.getFacetedCriteria(contextualParameters);
352        }
353    }
354    
355    @Override
356    public ViewItemContainer getResultItems(Map<String, Object> contextualParameters)
357    {
358        return Optional.ofNullable(_resultItems)
359                       .filter(viewItemContainer -> !(viewItemContainer.getViewItems().isEmpty()))
360                       .orElseGet(() -> _wrappedModel.getResultItems(contextualParameters));
361    }
362    
363    public ModelViewItem getResultItem(String itemPath, Map<String, Object> contextualParameters)
364    {
365        return Optional.ofNullable(_resultItems)
366                       .map(ri -> SearchModel.super.getResultItem(itemPath, contextualParameters))
367                       .orElseGet(() -> _wrappedModel.getResultItem(itemPath, contextualParameters));
368    }
369    
370    //// PROXY METHODS ////
371    
372    @Override
373    public Set<String> getContentTypes(Map<String, Object> contextualParameters)
374    {
375        return _wrappedModel.getContentTypes(contextualParameters);
376    }
377    
378    @Override
379    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
380    {
381        return _wrappedModel.getExcludedContentTypes(contextualParameters);
382    }
383    
384    @Override
385    public Map<String, ? extends SearchCriterion> getCriteria(Map<String, Object> contextualParameters)
386    {
387        return _wrappedModel.getCriteria(contextualParameters);
388    }
389    
390    @Override
391    public SearchCriterion getCriterion(String id, Map<String, Object> contextualParameters)
392    {
393        return _wrappedModel.getCriterion(id, contextualParameters);
394    }
395    
396    @Override
397    public String getWorkspace(Map<String, Object> contextualParameters)
398    {
399        return _wrappedModel.getWorkspace(contextualParameters);
400    }
401    
402    private ContentTypeExtensionPoint _getContentTypeExtensionPoint()
403    {
404        if (_contentTypeExtensionPoint == null)
405        {
406            try
407            {
408                _contentTypeExtensionPoint = (ContentTypeExtensionPoint) _serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
409            }
410            catch (ServiceException e)
411            {
412                throw new RuntimeException("Unable to lookup after the content type extension point", e);
413            }
414        }
415        
416        return _contentTypeExtensionPoint;
417    }
418    
419    private SystemPropertyExtensionPoint _getSystemPropertyExtensionPoint()
420    {
421        if (_systemPropertyExtensionPoint == null)
422        {
423            try
424            {
425                _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) _serviceManager.lookup(SystemPropertyExtensionPoint.ROLE);
426            }
427            catch (ServiceException e)
428            {
429                throw new RuntimeException("Unable to lookup after the system property extension point", e);
430            }
431        }
432        
433        return _systemPropertyExtensionPoint;
434    }
435    
436    private SearchUIModelExtensionPoint _getSearchUIModelExtensionPoint()
437    {
438        if (_searchUIModelExtensionPoint == null)
439        {
440            try
441            {
442                _searchUIModelExtensionPoint = (SearchUIModelExtensionPoint) _serviceManager.lookup(SearchUIModelExtensionPoint.ROLE);
443            }
444            catch (ServiceException e)
445            {
446                throw new RuntimeException("Unable to lookup after the search ui model extension point", e);
447            }
448        }
449        
450        return _searchUIModelExtensionPoint;
451    }
452    
453    private SearchCriterionHelper _getSearchCriterionHelper()
454    {
455        if (_searchCriterionHelper == null)
456        {
457            try
458            {
459                _searchCriterionHelper = (SearchCriterionHelper) _serviceManager.lookup(SearchCriterionHelper.ROLE);
460            }
461            catch (ServiceException e)
462            {
463                throw new RuntimeException("Unable to lookup after the search criterion helper", e);
464            }
465        }
466        
467        return _searchCriterionHelper;
468    }
469    
470    private ColumnHelper _getColumnHelper()
471    {
472        if (_columnHelper == null)
473        {
474            try
475            {
476                _columnHelper = (ColumnHelper) _serviceManager.lookup(ColumnHelper.ROLE);
477            }
478            catch (ServiceException e)
479            {
480                throw new RuntimeException("Unable to lookup after the column helper", e);
481            }
482        }
483        
484        return _columnHelper;
485    }
486}