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