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