001/*
002 *  Copyright 2019 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.web.frontoffice.search.metamodel.impl;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.List;
023import java.util.Optional;
024import java.util.function.Function;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.commons.lang3.StringUtils;
036
037import org.ametys.cms.content.ContentHelper;
038import org.ametys.cms.content.indexing.solr.SolrFieldNames;
039import org.ametys.cms.contenttype.ContentTypesHelper;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.search.SearchField;
042import org.ametys.cms.search.query.AndQuery;
043import org.ametys.cms.search.query.ContentLanguageQuery;
044import org.ametys.cms.search.query.ContentTypeQuery;
045import org.ametys.cms.search.query.DocumentTypeQuery;
046import org.ametys.cms.search.query.Query;
047import org.ametys.cms.search.ui.model.SearchUICriterion;
048import org.ametys.runtime.i18n.I18nizableText;
049import org.ametys.web.filter.ContentFilterHelper;
050import org.ametys.web.frontoffice.search.instance.model.SearchContext.LangQueryProducer;
051import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
052import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
053import org.ametys.web.frontoffice.search.metamodel.Returnable;
054import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
055import org.ametys.web.frontoffice.search.metamodel.Searchable;
056import org.ametys.web.frontoffice.search.metamodel.SearchableExtensionPoint;
057import org.ametys.web.frontoffice.search.metamodel.SortDefinition;
058import org.ametys.web.frontoffice.search.metamodel.context.ContextQueriesWrapper;
059import org.ametys.web.search.query.ContentPageQuery;
060
061import com.google.common.base.Predicates;
062
063/**
064 * Abstract class for all {@link Returnable} based on {@link Content}s
065 */
066public abstract class AbstractContentBasedReturnable extends AbstractParameterAdderReturnable implements Contextualizable, Initializable
067{
068    // Push ids of System Properties you do not want to appear in the sort list
069    private static final List<String> __EXCLUDED_SYSPROP_SORT_DEFINITIONS = Arrays.asList(
070            "lastValidation",
071            "firstValidation",
072            "lastModified",
073            "lastMajorValidation"
074    );
075    
076    /** The content filter helper */
077    protected ContentFilterHelper _contentFilterHelper;
078    /** The content helper */
079    protected ContentHelper _contentHelper;
080    /** The content types helper */
081    protected ContentTypesHelper _contentTypesHelper;
082    /** The extension point for searchables */
083    protected SearchableExtensionPoint _searchableEP;
084    /** The associated content searchable */
085    protected AbstractContentBasedSearchable _associatedContentSearchable;
086    
087    /** The context */
088    protected Context _context;
089    
090    /** The label */
091    protected I18nizableText _label;
092    
093    @Override
094    public void configure(Configuration configuration) throws ConfigurationException
095    {
096        super.configure(configuration);
097        _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + _pluginName);
098    }
099    
100    @Override
101    public void service(ServiceManager manager) throws ServiceException
102    {
103        super.service(manager);
104        _contentFilterHelper = (ContentFilterHelper) manager.lookup(ContentFilterHelper.ROLE);
105        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
106        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
107        _searchableEP = (SearchableExtensionPoint) manager.lookup(SearchableExtensionPoint.ROLE);
108    }
109    
110    /**
111     * Get the content helper
112     * @return the content helper
113     */
114    public ContentHelper getContentHelper()
115    {
116        return _contentHelper;
117    }
118    
119    /**
120     * Sets {@link #_associatedContentSearchable}. Called during {@link #initialize()}
121     */
122    protected void _setAssociatedContentSearchable()
123    {
124        _associatedContentSearchable = (AbstractContentBasedSearchable) _searchableEP.getExtension(associatedContentSearchableRole());
125    }
126    
127    /**
128     * The Avalon Role for the associated Content Searchable
129     * @return The Avalon Role for the associated Content Searchable
130     */
131    protected abstract String associatedContentSearchableRole();
132    
133    @Override
134    public void initialize() throws Exception
135    {
136        _setAssociatedContentSearchable();
137    }
138    
139    @Override
140    public void contextualize(Context context) throws ContextException
141    {
142        _context = context;
143    }
144    
145    @Override
146    public I18nizableText getLabel()
147    {
148        return _label;
149    }
150    
151    @Override
152    public Query filterReturnedDocumentQuery(Collection<ContextQueriesWrapper> contextQueriesWrappers, AdditionalParameterValueMap additionalParameterValues)
153    {
154        Collection<String> contentTypes = getContentTypes(additionalParameterValues);
155        
156        Function<Query, Query> siteQueryJoiner = siteQueryJoiner();
157        Function<Query, Query> sitemapQueryJoiner = sitemapQueryJoiner();
158        LangQueryProducer langQueryProducer = langQueryProducer();
159        Function<Query, Query> tagQueryJoiner = tagQueryJoiner();
160        Query contextQuery = ContextQueriesWrapper.getQuery(contextQueriesWrappers, Optional.ofNullable(siteQueryJoiner), Optional.ofNullable(sitemapQueryJoiner), Optional.ofNullable(langQueryProducer), Optional.ofNullable(tagQueryJoiner));
161        
162        List<Query> queries = new ArrayList<>();
163        queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
164        if (contentTypes != null)
165        {
166            queries.add(new ContentTypeQuery(contentTypes));
167        }
168        queries.add(contextQuery);
169        return new AndQuery(queries);
170    }
171    
172    /**
173     * The joiner for the site query
174     * @return The joiner for the site query
175     */
176    protected Function<Query, Query> siteQueryJoiner()
177    {
178        // site property is on contents via org.ametys.web.search.solr.field.SiteSearchField
179        return Function.identity();
180    }
181    
182    /**
183     * The joiner for the sitemap query
184     * @return The joiner for the sitemap query
185     */
186    protected Function<Query, Query> sitemapQueryJoiner()
187    {
188        // A SitemapQuery is on pages
189        return ContentPageQuery::new;
190    }
191    
192    /**
193     * The producer of the lang query
194     * @return The producer of the lang query
195     */
196    protected LangQueryProducer langQueryProducer()
197    {
198        // lang query
199        return new LangQueryProducer(ContentLanguageQuery.class, true);
200    }
201    
202    /**
203     * The joiner for the tag query
204     * @return The joiner for the tag query
205     */
206    protected Function<Query, Query> tagQueryJoiner()
207    {
208        // tag query is on contents
209        return Function.identity();
210    }
211    
212    /**
213     * Gets the content types which will be used to construct the query to filter the returned documents
214     * @param additionalParameterValues The additional parameter values
215     * @return the content types which will be used to construct the query to filter the returned documents
216     */
217    protected abstract Collection<String> getContentTypes(AdditionalParameterValueMap additionalParameterValues);
218    
219    @Override
220    public Collection<FacetDefinition> getFacets(AdditionalParameterValueMap additionalParameterValues)
221    {
222        Collection<SearchCriterionDefinition> criteria = _associatedContentSearchable.getCriteria(additionalParameterValues);
223        Collection<FacetDefinition> facetDefs = criteria.stream()
224                .filter(ContentSearchCriterionDefinition.class::isInstance)
225                .map(ContentSearchCriterionDefinition.class::cast)
226                .filter(critDef -> critDef.getSearchUICriterion().isFacetable())
227                .map(critDef -> new ContentFacetDefinition(critDef, this))
228                .collect(Collectors.toList());
229        
230        return facetDefs;
231    }
232    
233    @Override
234    public Collection<SortDefinition> getSorts(AdditionalParameterValueMap additionalParameterValues)
235    {
236        final String titleCriterionDefId = _associatedContentSearchable.getIndexingFieldCriterionDefinitionPrefix() + "_common$title";
237        Collection<SearchCriterionDefinition> criteria = _associatedContentSearchable.getCriteria(additionalParameterValues);
238        Collection<SortDefinition> sortDefs = criteria.stream()
239                .filter(ContentSearchCriterionDefinition.class::isInstance)
240                .map(ContentSearchCriterionDefinition.class::cast)
241                .filter(critDef -> critDef.getSearchUICriterion().isSortable())
242                .filter(critDef -> !titleCriterionDefId.equals(critDef.getId()))
243                .filter(this::_isSortable)
244                .filter(Predicates.not(this::_isExcludedSortDefinition))
245                .map(critDef -> new DefaultSortDefinition(getDefinitionPrefix() + critDef.getId(), critDef.getLabel(), critDef.getSearchUICriterion().getSearchField()))
246                .collect(Collectors.toList());
247                
248        return sortDefs;
249    }
250    
251    /**
252     * Gets the prefix for definitions (for facets, sorts...)
253     * @return the prefix for definitions
254     */
255    protected abstract String getDefinitionPrefix();
256    
257    private boolean _isSortable(ContentSearchCriterionDefinition criterionDefinition)
258    {
259        // some can return true to #getSearchUICriterion()#isSortable()
260        // but can return null to #getSearchUICriterion()#getSearchField()#getSortField()
261        // so we need to do this additional test
262        return Optional.ofNullable(criterionDefinition)
263                .map(ContentSearchCriterionDefinition::getSearchUICriterion)
264                .map(SearchUICriterion::getSearchField)
265                .map(SearchField::getSortField)
266                .isPresent();
267    }
268    
269    private boolean _isExcludedSortDefinition(SearchCriterionDefinition criterionDefinition)
270    {
271        String critId = criterionDefinition.getId();
272        String sysPropId = StringUtils.substringAfter(critId, _associatedContentSearchable.getSystemPropertyCriterionDefinitionPrefix());
273        return __EXCLUDED_SYSPROP_SORT_DEFINITIONS.contains(sysPropId);
274    }
275    
276    @Override
277    public Collection<Searchable> relationsWith()
278    {
279        return Collections.singleton(_associatedContentSearchable);
280    }
281}