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.Set;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.commons.lang3.StringUtils;
032import org.slf4j.Logger;
033
034import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
035import org.ametys.cms.search.query.Query.Operator;
036import org.ametys.cms.search.ui.model.AbstractSearchUIModel;
037import org.ametys.cms.search.ui.model.SearchUIColumn;
038import org.ametys.cms.search.ui.model.SearchUICriterion;
039import org.ametys.cms.search.ui.model.SearchUIModel;
040import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
041import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
042import org.ametys.cms.search.ui.model.impl.MetadataSearchUIColumn;
043import org.ametys.cms.search.ui.model.impl.SystemSearchUIColumn;
044import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
045import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
046
047/**
048 * Search model wrapper which handles custom on-the-fly columns and facets.
049 */
050public class CriteriaSearchUIModelWrapper extends AbstractSearchUIModel
051{
052    
053    /** ComponentManager for {@link SearchUICriterion}s. */
054    protected ThreadSafeComponentManager<SearchUICriterion> _searchUICriterionManager;
055    
056    /** ComponentManager for {@link SearchUIColumn}s. */
057    protected ThreadSafeComponentManager<SearchUIColumn> _searchUIColumnManager;
058    
059    private SearchUIModelExtensionPoint _searchModelEP;
060    private SystemPropertyExtensionPoint _sysPropEP;
061    
062    private SearchUIModel _wrappedModel;
063    
064    private int _criteriaIndex;
065    
066    /**
067     * Build a model wrapper.
068     * @param model the search model to wrap.
069     * @param manager the service manager.
070     * @param context the component context.
071     * @param logger the logger.
072     */
073    public CriteriaSearchUIModelWrapper(SearchUIModel model, ServiceManager manager, Context context, Logger logger)
074    {
075        _wrappedModel = model;
076        
077        _logger = logger;
078        
079        try
080        {
081            _searchUICriterionManager = new ThreadSafeComponentManager<>();
082            _searchUICriterionManager.setLogger(logger);
083            _searchUICriterionManager.contextualize(context);
084            _searchUICriterionManager.service(manager);
085            
086            _searchUIColumnManager = new ThreadSafeComponentManager<>();
087            _searchUIColumnManager.setLogger(logger);
088            _searchUIColumnManager.contextualize(context);
089            _searchUIColumnManager.service(manager);
090        }
091        catch (Exception e)
092        {
093            _logger.error("Error initializing the SearchModel", e);
094        }
095    }
096    
097    @Override
098    public void service(ServiceManager manager) throws ServiceException
099    {
100        super.service(manager);
101        _searchModelEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE);
102        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
103    }
104    
105    /**
106     * Set the custom faceted criteria.
107     * @param contentTypeId the reference content type ID, can be null.
108     * @param criterionIds the criterion IDs
109     * @param contextualParameters the contextual parameters
110     * @throws Exception if an error occurs initializing criteria.
111     */
112    public void setFacetedCriteria(String contentTypeId, Collection<String> criterionIds, Map<String, Object> contextualParameters) throws Exception
113    {
114        if (criterionIds != null)
115        {
116            _facetedCriteria = new LinkedHashMap<>(criterionIds.size());
117            
118            List<Object> searchToolCriterionRoles = new ArrayList<>();
119            
120            configureFacets(searchToolCriterionRoles, contentTypeId, criterionIds, _wrappedModel, contextualParameters);
121            
122            _searchUICriterionManager.initialize();
123            
124            for (Object critObj : searchToolCriterionRoles)
125            {
126                SearchUICriterion criterion = null;
127                if (critObj instanceof SearchUICriterion)
128                {
129                    // Already existing SearchUICriterion object (taken from the wrapped model).
130                    criterion = (SearchUICriterion) critObj;
131                }
132                else if (critObj instanceof String)
133                {
134                    // Criterion just added in the local component manager, we have to look it up.
135                    criterion = _searchUICriterionManager.lookup((String) critObj);
136                }
137                
138                if (criterion != null && criterion.isFacetable())
139                {
140                    _facetedCriteria.put(criterion.getId(), criterion);
141                }
142            }
143        }
144    }
145    
146    /**
147     * Set the custom columns.
148     * @param contentTypeId the reference content type ID, can be null.
149     * @param columnIds The column IDs
150     * @param contextualParameters the contextual parameters
151     * @throws Exception if an error occurs initializing columns.
152     */
153    public void setResultColumns(String contentTypeId, Collection<String> columnIds, Map<String, Object> contextualParameters) throws Exception
154    {
155        String wrappedModelId = (String) contextualParameters.get("wrappedModelId");
156        if (StringUtils.isNotEmpty(wrappedModelId))
157        {
158            // Dashboard
159            SearchUIModel model = _searchModelEP.getExtension(wrappedModelId);
160            _columns = model.getResultFields(contextualParameters);
161        }
162        else if (columnIds != null)
163        {
164            _columns = new LinkedHashMap<>(columnIds.size());
165            
166            List<Object> columnRoles = new ArrayList<>();
167            
168            configureColumns(columnRoles, contentTypeId, columnIds, _wrappedModel, contextualParameters);
169            
170            _searchUIColumnManager.initialize();
171            
172            for (Object col : columnRoles)
173            {
174                SearchUIColumn column = null;
175                if (col instanceof SearchUIColumn)
176                {
177                    // Already existing SearchUIColumn object (taken from the wrapped model).
178                    column = (SearchUIColumn) col;
179                }
180                else if (col instanceof String)
181                {
182                    // Column just added in the local component manager, we have to look it up.
183                    column = _searchUIColumnManager.lookup((String) col);
184                }
185
186                if (column != null)
187                {
188                    _columns.put(column.getId(), column);
189                }
190            }
191        }
192    }
193    
194    /**
195     * Configure the list of faceted criteria.
196     * @param criterionRoles the roles of criteria to lookup (or the already existing SearchUICriterion objects).
197     * @param contentTypeId The reference content type ID.
198     * @param criterionIds the criterion IDs.
199     * @param referenceModel the reference model.
200     * @param contextualParameters the contextual parameters
201     * @throws ConfigurationException if an error occurs creating a component configuration.
202     */
203    protected void configureFacets(List<Object> criterionRoles, String contentTypeId, Collection<String> criterionIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException
204    {
205        for (String criterionId : criterionIds)
206        {
207            SearchUICriterion referenceCriteria = null;
208            if (referenceModel != null)
209            {
210                referenceCriteria = getCriterion(referenceModel, criterionId, contextualParameters);
211            }
212            
213            if (referenceCriteria != null)
214            {
215                criterionRoles.add(referenceCriteria);
216            }
217            else if (_sysPropEP.isSearchable(criterionId))
218            {
219                // System property.
220                addSystemCriteriaComponents(criterionRoles, contentTypeId, criterionId);
221            }
222            else
223            {
224                if (contentTypeId != null && _cTypeEP.hasExtension(contentTypeId))
225                {
226                    // Metadata property.
227                    addIndexingFieldCriteriaComponents(criterionRoles, contentTypeId, criterionId);
228                }
229                else if ("title".equals(criterionId))
230                {
231                    // title property of a random ContentType.
232                    String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next();
233                    addIndexingFieldCriteriaComponents(criterionRoles, firstCTypeId, criterionId);
234                }
235            }
236        }
237    }
238    
239    /**
240     * Search a criterion in the reference model from its criterion identifier.
241     * @param searchModel the reference search model.
242     * @param criterionId the criterion identifier.
243     * @param contextualParameters the contextual parameters
244     * @return the criterion if found, null otherwise.
245     */
246    protected SearchUICriterion getCriterion(SearchUIModel searchModel, String criterionId, Map<String, Object> contextualParameters)
247    {
248        Map<String, SearchUICriterion> criteria = searchModel.getFacetedCriteria(contextualParameters);
249        
250        for (SearchUICriterion criterion : criteria.values())
251        {
252            if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId))
253            {
254                return criterion;
255            }
256            else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId))
257            {
258                return criterion;
259            }
260            else if (criterion.getId().equals(criterionId))
261            {
262                return criterion;
263            }
264        }
265        
266        return null;
267    }
268    
269    /**
270     * Configure the list of search columns.
271     * @param columnRoles the roles of columns to lookup (or the already existing SearchUIColumn objects).
272     * @param contentTypeId The reference content type ID.
273     * @param columnIds the column IDs.
274     * @param referenceModel the reference model.
275     * @param contextualParameters the contextual parameters
276     * @throws ConfigurationException if an error occurs creating a component configuration.
277     */
278    protected void configureColumns(List<Object> columnRoles, String contentTypeId, Collection<String> columnIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException
279    {
280        for (String columnId : columnIds)
281        {
282            SearchUIColumn referenceColumn = null;
283            if (referenceModel != null)
284            {
285                referenceColumn = getColumn(referenceModel, columnId, contextualParameters);
286            }
287            
288            if (referenceColumn != null)
289            {
290                columnRoles.add(referenceColumn);
291            }
292            else if (_sysPropEP.isDisplayable(columnId))
293            {
294                // System property.
295                addSystemColumnComponent(columnRoles, contentTypeId, columnId);
296            }
297            else
298            {
299                // Metadata property.
300                if (contentTypeId != null && _cTypeEP.hasExtension(contentTypeId))
301                {
302                    addMetadataColumnComponents(columnRoles, contentTypeId, columnId);
303                }
304                else if ("title".equals(columnId))
305                {
306                    // Get the title property of a random ContentType.
307                    String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next();
308                    addMetadataColumnComponents(columnRoles, firstCTypeId, columnId);
309                }
310            }
311        }
312    }
313    
314    /**
315     * Search a column in the reference model from its column identifier.
316     * @param searchModel the reference search model.
317     * @param columnId the column identifier.
318     * @param contextualParameters the contextual parameters
319     * @return the column if found, null otherwise.
320     */
321    protected SearchUIColumn getColumn(SearchUIModel searchModel, String columnId, Map<String, Object> contextualParameters)
322    {
323        Map<String, SearchUIColumn> columns = searchModel.getResultFields(contextualParameters);
324        
325        for (SearchUIColumn column : columns.values())
326        {
327            if (column instanceof MetadataSearchUIColumn && ((MetadataSearchUIColumn) column).getFieldPath().equals(columnId))
328            {
329                return column;
330            }
331            else if (column instanceof SystemSearchUIColumn && ((SystemSearchUIColumn) column).getSystemPropertyId().equals(columnId))
332            {
333                return column;
334            }
335//            else if (column instanceof CustomSearchToolColumn && column.getId().equals(columnId))
336//            {
337//                return column;
338//            }
339        }
340        
341        return null;
342    }
343
344    /**
345     * Add a indexing field criteria component to the manager.
346     * @param searchToolCriterionRoles the criteria role list to fill.
347     * @param contentTypeId the reference content type ID, can be null.
348     * @param fieldRef the field path.
349     * @throws ConfigurationException if an error occurs.
350     */
351    protected void addIndexingFieldCriteriaComponents(List<Object> searchToolCriterionRoles, String contentTypeId, String fieldRef) throws ConfigurationException
352    {
353        try
354        {
355            String slashPath = fieldRef.replace('.', '/');
356            
357            String role = fieldRef + _criteriaIndex;
358            _criteriaIndex++;
359            Configuration criteriaConf = getIndexingFieldCriteriaConfiguration(contentTypeId, slashPath, Operator.EQ);
360            
361            _searchUICriterionManager.addComponent("search", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
362            
363            searchToolCriterionRoles.add(role);
364        }
365        catch (Exception e)
366        {
367            throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, e);
368        }        
369    }
370    
371    /**
372     * Add a system criteria component to the manager.
373     * @param searchToolCriterionRoles the criteria role list to fill.
374     * @param contentTypeId the reference content type ID, can be null.
375     * @param property the system property id.
376     * @throws ConfigurationException if an error occurs.
377     */
378    protected void addSystemCriteriaComponents(List<Object> searchToolCriterionRoles, String contentTypeId, String property) throws ConfigurationException
379    {
380        try
381        {
382            String role = property + _criteriaIndex;
383            _criteriaIndex++;
384            
385            Configuration criteriaConf = getSystemCriteriaConfiguration(contentTypeId, property);
386            _searchUICriterionManager.addComponent("search", null, role, SystemSearchUICriterion.class, criteriaConf);
387            
388            searchToolCriterionRoles.add(role);
389        }
390        catch (Exception e)
391        {
392            throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, e);
393        }
394    }
395    
396    /**
397     * Add a metadata column component to the manager.
398     * @param columnsRolesToLookup the columns roles
399     * @param contentTypeId the reference content type ID, can be null.
400     * @param metadataPath the metadata path.
401     * @throws ConfigurationException if an error occurs.
402     */
403    protected void addMetadataColumnComponents(List<Object> columnsRolesToLookup, String contentTypeId, String metadataPath) throws ConfigurationException
404    {
405        try
406        {
407            String slashPath = metadataPath.replace('.', '/');
408            
409            Configuration columnConf = getMetadataColumnConfiguration(contentTypeId, slashPath);
410            
411            _searchUIColumnManager.addComponent("search", null, metadataPath, MetadataSearchUIColumn.class, columnConf);
412            columnsRolesToLookup.add(metadataPath);
413        }
414        catch (Exception e)
415        {
416            throw new ConfigurationException("Unable to instanciate MetadataSearchUIColumn for metadata " + metadataPath, e);
417        }
418    }
419    
420    /**
421     * Add a system column component to the manager.
422     * @param columnsRolesToLookup the columns roles
423     * @param contentTypeId the reference content type ID, can be null.
424     * @param property the system property.
425     * @throws ConfigurationException if an error occurs.
426     */
427    protected void addSystemColumnComponent(List<Object> columnsRolesToLookup, String contentTypeId, String property) throws ConfigurationException
428    {
429        try
430        {
431            Configuration conf = getSystemColumnConfiguration(contentTypeId, property);
432            _searchUIColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf);
433            columnsRolesToLookup.add(property);
434        }
435        catch (Exception e)
436        {
437            throw new ConfigurationException("Unable to instanciate SystemSearchUIColumn for property " + property, e);
438        }
439    }
440    
441    @Override
442    public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters)
443    {
444        if (_facetedCriteria != null && !_facetedCriteria.isEmpty())
445        {
446            return Collections.unmodifiableMap(_facetedCriteria);
447        }
448        else
449        {
450            return _wrappedModel.getFacetedCriteria(contextualParameters);
451        }
452    }
453    
454    @Override
455    public Map<String, SearchUIColumn> getResultFields(Map<String, Object> contextualParameters)
456    {
457        if (_columns != null && !_columns.isEmpty())
458        {
459            return Collections.unmodifiableMap(_columns);
460        }
461        else
462        {
463            return _wrappedModel.getResultFields(contextualParameters);
464        }
465    }
466    
467    @Override
468    public SearchUIColumn getResultField(String id, Map<String, Object> contextualParameters)
469    {
470        if (_columns != null && !_columns.isEmpty())
471        {
472            return getResultFields(contextualParameters).get(id);
473        }
474        else
475        {
476            return _wrappedModel.getResultField(id, contextualParameters);
477        }
478    }
479    
480    //// PROXY METHODS ////
481    
482    @Override
483    public Set<String> getContentTypes(Map<String, Object> contextualParameters)
484    {
485        return _wrappedModel.getContentTypes(contextualParameters);
486    }
487    
488    @Override
489    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
490    {
491        return _wrappedModel.getExcludedContentTypes(contextualParameters);
492    }
493    
494    @Override
495    public String getSearchUrl(Map<String, Object> contextualParameters)
496    {
497        return _wrappedModel.getSearchUrl(contextualParameters);
498    }
499
500    @Override
501    public String getSearchUrlPlugin(Map<String, Object> contextualParameters)
502    {
503        return _wrappedModel.getSearchUrlPlugin(contextualParameters);
504    }
505
506    @Override
507    public String getExportCSVUrl(Map<String, Object> contextualParameters)
508    {
509        return _wrappedModel.getExportCSVUrl(contextualParameters);
510    }
511
512    @Override
513    public String getExportCSVUrlPlugin(Map<String, Object> contextualParameters)
514    {
515        return _wrappedModel.getExportCSVUrlPlugin(contextualParameters);
516    }
517
518    @Override
519    public String getExportXMLUrl(Map<String, Object> contextualParameters)
520    {
521        return _wrappedModel.getExportXMLUrl(contextualParameters);
522    }
523
524    @Override
525    public String getExportXMLUrlPlugin(Map<String, Object> contextualParameters)
526    {
527        return _wrappedModel.getExportXMLUrlPlugin(contextualParameters);
528    }
529
530    @Override
531    public String getPrintUrl(Map<String, Object> contextualParameters)
532    {
533        return _wrappedModel.getPrintUrl(contextualParameters);
534    }
535
536    @Override
537    public String getPrintUrlPlugin(Map<String, Object> contextualParameters)
538    {
539        return _wrappedModel.getPrintUrlPlugin(contextualParameters);
540    }
541
542    @Override
543    public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters)
544    {
545        return _wrappedModel.getCriteria(contextualParameters);
546    }
547    
548    @Override
549    public SearchUICriterion getCriterion(String id, Map<String, Object> contextualParameters)
550    {
551        return _wrappedModel.getCriterion(id, contextualParameters);
552    }
553    
554    @Override
555    public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters)
556    {
557        return _wrappedModel.getAdvancedCriteria(contextualParameters);
558    }
559    
560}