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; 026 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.context.Context; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.commons.lang3.StringUtils; 033import org.slf4j.Logger; 034 035import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.model.SearchCriterion; 038import org.ametys.cms.search.model.SearchCriterionHelper; 039import org.ametys.cms.search.model.SearchModel; 040import org.ametys.cms.search.model.SystemProperty; 041import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 042import org.ametys.cms.search.query.Query.Operator; 043import org.ametys.cms.search.ui.model.ColumnHelper; 044import org.ametys.cms.search.ui.model.ColumnHelper.Column; 045import org.ametys.cms.search.ui.model.SearchUICriterion; 046import org.ametys.cms.search.ui.model.SearchUIModel; 047import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 048import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion; 049import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 050import org.ametys.runtime.model.ModelViewItem; 051import org.ametys.runtime.model.ViewItemContainer; 052import org.ametys.runtime.plugin.component.LogEnabled; 053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 054 055/** 056 * Search model wrapper which handles custom on-the-fly columns and facets. 057 */ 058public class CriteriaSearchModelWrapper implements SearchModel, LogEnabled 059{ 060 /** ComponentManager for {@link SearchUICriterion}s. */ 061 protected ThreadSafeComponentManager<SearchCriterion> _searchCriterionManager; 062 063 /** The logger. */ 064 protected Logger _logger; 065 066 private ServiceManager _serviceManager; 067 068 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 069 private SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 070 private SearchUIModelExtensionPoint _searchUIModelExtensionPoint; 071 private SearchCriterionHelper _searchCriterionHelper; 072 private ColumnHelper _columnHelper; 073 074 private SearchModel _wrappedModel; 075 076 /** The search criteria used as facets, indexed by ID. */ 077 private Map<String, SearchCriterion> _facetedCriteria; 078 /** The result items */ 079 private ViewItemContainer _resultItems; 080 081 private int _criteriaIndex; 082 083 /** 084 * Build a model wrapper. 085 * @param model the search model to wrap. 086 * @param manager the service manager. 087 * @param context the component context. 088 * @param logger the logger. 089 */ 090 public CriteriaSearchModelWrapper(SearchModel model, ServiceManager manager, Context context, Logger logger) 091 { 092 _wrappedModel = model; 093 094 _serviceManager = manager; 095 _logger = logger; 096 097 try 098 { 099 _searchCriterionManager = new ThreadSafeComponentManager<>(); 100 _searchCriterionManager.setLogger(logger); 101 _searchCriterionManager.contextualize(context); 102 _searchCriterionManager.service(manager); 103 } 104 catch (Exception e) 105 { 106 _logger.error("Error initializing the SearchModel", e); 107 } 108 } 109 110 public void setLogger(final Logger logger) 111 { 112 _logger = logger; 113 } 114 115 /** 116 * Get the logger. 117 * @return the logger. 118 */ 119 protected final Logger getLogger() 120 { 121 return _logger; 122 } 123 124 /** 125 * Set the custom faceted criteria. 126 * @param baseContentTypeIds the reference content type identifiers. 127 * @param criteriaIds the criteria IDs 128 * @param contextualParameters the contextual parameters 129 * @return the valid criteria IDs 130 * @throws Exception if an error occurs initializing criteria. 131 */ 132 public Collection<String> setFacetedCriteria(Set<String> baseContentTypeIds, Collection<String> criteriaIds, Map<String, Object> contextualParameters) throws Exception 133 { 134 Collection<String> resultCriteriaIds = null; 135 136 if (criteriaIds != null) 137 { 138 _facetedCriteria = new LinkedHashMap<>(criteriaIds.size()); 139 140 List<Object> searchToolCriterionRoles = new ArrayList<>(); 141 142 resultCriteriaIds = configureFacets(searchToolCriterionRoles, baseContentTypeIds, criteriaIds, _wrappedModel, contextualParameters); 143 144 _searchCriterionManager.initialize(); 145 146 for (Object critObj : searchToolCriterionRoles) 147 { 148 SearchCriterion criterion = null; 149 if (critObj instanceof SearchCriterion) 150 { 151 // Already existing SearchCriterion object (taken from the wrapped model). 152 criterion = (SearchCriterion) critObj; 153 } 154 else if (critObj instanceof String) 155 { 156 // Criterion just added in the local component manager, we have to look it up. 157 criterion = _searchCriterionManager.lookup((String) critObj); 158 } 159 160 if (criterion != null && criterion.isFacetable()) 161 { 162 _facetedCriteria.put(criterion.getId(), criterion); 163 } 164 } 165 } 166 167 return resultCriteriaIds != null ? resultCriteriaIds : Collections.emptySet(); 168 } 169 170 /** 171 * Set the custom columns. 172 * @param baseContentTypeIds the reference content type identifiers. 173 * @param columns The columns 174 * @param contextualParameters the contextual parameters 175 * @throws Exception if an error occurs initializing columns. 176 */ 177 public void setResultColumns(Set<String> baseContentTypeIds, Collection<Column> columns, Map<String, Object> contextualParameters) throws Exception 178 { 179 String wrappedModelId = (String) contextualParameters.get("wrappedModelId"); 180 if (StringUtils.isNotEmpty(wrappedModelId)) 181 { 182 // Dashboard 183 SearchUIModel model = _getSearchUIModelExtensionPoint().getExtension(wrappedModelId); 184 _resultItems = model.getResultItems(contextualParameters); 185 } 186 else if (columns != null) 187 { 188 _resultItems = _getColumnHelper().createViewFromColumns(baseContentTypeIds, columns, false); 189 } 190 } 191 192 /** 193 * Configure the list of faceted criteria. 194 * @param criteriaRoles the roles of criteria to lookup (or the already existing SearchUICriterion objects). 195 * @param baseContentTypeIds The reference content type identifiers. 196 * @param criteriaIds the criteria IDs. 197 * @param referenceModel the reference model. 198 * @param contextualParameters the contextual parameters 199 * @return the valid criteria IDs 200 * @throws ConfigurationException if an error occurs creating a component configuration. 201 */ 202 protected Collection<String> configureFacets(List<Object> criteriaRoles, Set<String> baseContentTypeIds, Collection<String> criteriaIds, SearchModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException 203 { 204 Collection<String> resultCriteriaIds = new ArrayList<>(); 205 206 for (String criterionId : criteriaIds) 207 { 208 String[] facetPathSegments = StringUtils.split(criterionId, '/'); 209 String lastSegmentOfFacetPath = facetPathSegments[facetPathSegments.length - 1]; 210 211 SearchCriterion referenceCriterion = null; 212 if (referenceModel != null) 213 { 214 referenceCriterion = getCriterion(referenceModel, criterionId, contextualParameters); 215 } 216 217 if (referenceCriterion != null) 218 { 219 criteriaRoles.add(referenceCriterion); 220 } 221 else if (_getSystemPropertyExtensionPoint().hasExtension(lastSegmentOfFacetPath)) 222 { 223 SystemProperty systemProperty = _getSystemPropertyExtensionPoint().getExtension(lastSegmentOfFacetPath); 224 if (systemProperty.isFacetable()) 225 { 226 addSystemCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId); 227 } 228 else 229 { 230 getLogger().warn("The declared facet '{}' is a system property but is not facetable. Thus, it will not be added to the facets.", criterionId); 231 break; 232 } 233 } 234 else 235 { 236 if (!baseContentTypeIds.isEmpty()) 237 { 238 // Metadata property. 239 addIndexingFieldCriteriaComponents(criteriaRoles, baseContentTypeIds, criterionId); 240 } 241 else if (Content.ATTRIBUTE_TITLE.equals(criterionId)) 242 { 243 // title property of a random ContentType. 244 String firstCTypeId = _getContentTypeExtensionPoint().getExtensionsIds().iterator().next(); 245 addIndexingFieldCriteriaComponents(criteriaRoles, Set.of(firstCTypeId), criterionId); 246 } 247 else 248 { 249 break; 250 } 251 } 252 253 resultCriteriaIds.add(criterionId); 254 } 255 256 return resultCriteriaIds; 257 } 258 259 /** 260 * Search a criterion in the reference model from its criterion identifier. 261 * @param searchModel the reference search model. 262 * @param criterionId the criterion identifier. 263 * @param contextualParameters the contextual parameters 264 * @return the criterion if found, null otherwise. 265 */ 266 protected SearchCriterion getCriterion(SearchModel searchModel, String criterionId, Map<String, Object> contextualParameters) 267 { 268 Map<String, ? extends SearchCriterion> criteria = searchModel.getFacetedCriteria(contextualParameters); 269 270 for (SearchCriterion criterion : criteria.values()) 271 { 272 if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId)) 273 { 274 return criterion; 275 } 276 else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId)) 277 { 278 return criterion; 279 } 280 else if (criterion.getId().equals(criterionId)) 281 { 282 return criterion; 283 } 284 } 285 286 return null; 287 } 288 289 290 /** 291 * Add a indexing field criteria component to the manager. 292 * @param searchToolCriterionRoles the criteria role list to fill. 293 * @param baseContentTypeIds the reference content type identifiers. 294 * @param fieldRef the field path. 295 * @throws ConfigurationException if an error occurs. 296 */ 297 protected void addIndexingFieldCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String fieldRef) throws ConfigurationException 298 { 299 try 300 { 301 String slashPath = fieldRef.replace('.', '/'); 302 303 String role = fieldRef + _criteriaIndex; 304 _criteriaIndex++; 305 Configuration criteriaConf = _getSearchCriterionHelper().getIndexingFieldCriteriaConfiguration(this, Optional.empty(), baseContentTypeIds, slashPath, Optional.of(Operator.EQ), Optional.empty()); 306 307 _searchCriterionManager.addComponent("search", null, role, IndexingFieldSearchUICriterion.class, criteriaConf); 308 309 searchToolCriterionRoles.add(role); 310 } 311 catch (Exception e) 312 { 313 throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, e); 314 } 315 } 316 317 /** 318 * Add a system criteria component to the manager. 319 * @param searchToolCriterionRoles the criteria role list to fill. 320 * @param baseContentTypeIds the reference content type identifiers. 321 * @param property the system property id. 322 * @throws ConfigurationException if an error occurs. 323 */ 324 protected void addSystemCriteriaComponents(List<Object> searchToolCriterionRoles, Set<String> baseContentTypeIds, String property) throws ConfigurationException 325 { 326 try 327 { 328 String role = property + _criteriaIndex; 329 _criteriaIndex++; 330 331 Configuration criteriaConf = _getSearchCriterionHelper().getSystemCriteriaConfiguration(this, Optional.empty(), baseContentTypeIds, property, Optional.empty()); 332 _searchCriterionManager.addComponent("search", null, role, SystemSearchUICriterion.class, criteriaConf); 333 334 searchToolCriterionRoles.add(role); 335 } 336 catch (Exception e) 337 { 338 throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, e); 339 } 340 } 341 342 @Override 343 public Map<String, ? extends SearchCriterion> getFacetedCriteria(Map<String, Object> contextualParameters) 344 { 345 if (_facetedCriteria != null && !_facetedCriteria.isEmpty()) 346 { 347 return Collections.unmodifiableMap(_facetedCriteria); 348 } 349 else 350 { 351 return _wrappedModel.getFacetedCriteria(contextualParameters); 352 } 353 } 354 355 @Override 356 public ViewItemContainer getResultItems(Map<String, Object> contextualParameters) 357 { 358 return Optional.ofNullable(_resultItems) 359 .filter(viewItemContainer -> !(viewItemContainer.getViewItems().isEmpty())) 360 .orElseGet(() -> _wrappedModel.getResultItems(contextualParameters)); 361 } 362 363 public ModelViewItem getResultItem(String itemPath, Map<String, Object> contextualParameters) 364 { 365 return Optional.ofNullable(_resultItems) 366 .map(ri -> SearchModel.super.getResultItem(itemPath, contextualParameters)) 367 .orElseGet(() -> _wrappedModel.getResultItem(itemPath, contextualParameters)); 368 } 369 370 //// PROXY METHODS //// 371 372 @Override 373 public Set<String> getContentTypes(Map<String, Object> contextualParameters) 374 { 375 return _wrappedModel.getContentTypes(contextualParameters); 376 } 377 378 @Override 379 public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters) 380 { 381 return _wrappedModel.getExcludedContentTypes(contextualParameters); 382 } 383 384 @Override 385 public Map<String, ? extends SearchCriterion> getCriteria(Map<String, Object> contextualParameters) 386 { 387 return _wrappedModel.getCriteria(contextualParameters); 388 } 389 390 @Override 391 public SearchCriterion getCriterion(String id, Map<String, Object> contextualParameters) 392 { 393 return _wrappedModel.getCriterion(id, contextualParameters); 394 } 395 396 @Override 397 public String getWorkspace(Map<String, Object> contextualParameters) 398 { 399 return _wrappedModel.getWorkspace(contextualParameters); 400 } 401 402 private ContentTypeExtensionPoint _getContentTypeExtensionPoint() 403 { 404 if (_contentTypeExtensionPoint == null) 405 { 406 try 407 { 408 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) _serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 409 } 410 catch (ServiceException e) 411 { 412 throw new RuntimeException("Unable to lookup after the content type extension point", e); 413 } 414 } 415 416 return _contentTypeExtensionPoint; 417 } 418 419 private SystemPropertyExtensionPoint _getSystemPropertyExtensionPoint() 420 { 421 if (_systemPropertyExtensionPoint == null) 422 { 423 try 424 { 425 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) _serviceManager.lookup(SystemPropertyExtensionPoint.ROLE); 426 } 427 catch (ServiceException e) 428 { 429 throw new RuntimeException("Unable to lookup after the system property extension point", e); 430 } 431 } 432 433 return _systemPropertyExtensionPoint; 434 } 435 436 private SearchUIModelExtensionPoint _getSearchUIModelExtensionPoint() 437 { 438 if (_searchUIModelExtensionPoint == null) 439 { 440 try 441 { 442 _searchUIModelExtensionPoint = (SearchUIModelExtensionPoint) _serviceManager.lookup(SearchUIModelExtensionPoint.ROLE); 443 } 444 catch (ServiceException e) 445 { 446 throw new RuntimeException("Unable to lookup after the search ui model extension point", e); 447 } 448 } 449 450 return _searchUIModelExtensionPoint; 451 } 452 453 private SearchCriterionHelper _getSearchCriterionHelper() 454 { 455 if (_searchCriterionHelper == null) 456 { 457 try 458 { 459 _searchCriterionHelper = (SearchCriterionHelper) _serviceManager.lookup(SearchCriterionHelper.ROLE); 460 } 461 catch (ServiceException e) 462 { 463 throw new RuntimeException("Unable to lookup after the search criterion helper", e); 464 } 465 } 466 467 return _searchCriterionHelper; 468 } 469 470 private ColumnHelper _getColumnHelper() 471 { 472 if (_columnHelper == null) 473 { 474 try 475 { 476 _columnHelper = (ColumnHelper) _serviceManager.lookup(ColumnHelper.ROLE); 477 } 478 catch (ServiceException e) 479 { 480 throw new RuntimeException("Unable to lookup after the column helper", e); 481 } 482 } 483 484 return _columnHelper; 485 } 486}