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}