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.content; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032 033import org.ametys.cms.content.indexing.solr.SolrFieldNames; 034import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 035import org.ametys.cms.contenttype.ContentTypesHelper; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.QueryBuilder; 038import org.ametys.cms.search.SearchField; 039import org.ametys.cms.search.SearchResults; 040import org.ametys.cms.search.Sort; 041import org.ametys.cms.search.Sort.Order; 042import org.ametys.cms.search.filter.AccessSearchFilter; 043import org.ametys.cms.search.model.ResultField; 044import org.ametys.cms.search.model.SearchCriterion; 045import org.ametys.cms.search.model.SearchModel; 046import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 047import org.ametys.cms.search.query.ContentTypeQuery; 048import org.ametys.cms.search.query.DocumentTypeQuery; 049import org.ametys.cms.search.query.Query; 050import org.ametys.cms.search.solr.SearcherFactory; 051import org.ametys.cms.search.solr.SearcherFactory.Searcher; 052import org.ametys.cms.search.ui.model.SearchUIModel; 053import org.ametys.plugins.repository.AmetysObject; 054import org.ametys.plugins.repository.AmetysObjectIterable; 055import org.ametys.runtime.plugin.component.AbstractLogEnabled; 056 057/** 058 * Component creating content searchers from {@link SearchModel}s or content type IDs. 059 */ 060public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable 061{ 062 063 /** The component role. */ 064 public static final String ROLE = ContentSearcherFactory.class.getName(); 065 066 /** The searcher factory. */ 067 protected SearcherFactory _searcherFactory; 068 069 /** The query builder. */ 070 protected QueryBuilder _queryBuilder; 071 072 /** The content type extension point. */ 073 protected ContentTypeExtensionPoint _cTypeEP; 074 075 /** The content type helper. */ 076 protected ContentTypesHelper _cTypeHelper; 077 078 /** The system property extension point. */ 079 protected SystemPropertyExtensionPoint _sysPropEP; 080 081 /** The search helper. */ 082 protected ContentSearchHelper _searchHelper; 083 084 @Override 085 public void service(ServiceManager manager) throws ServiceException 086 { 087 _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE); 088 _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE); 089 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 090 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 091 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 092 093 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 094 } 095 096 /** 097 * Create a ContentSearcher from a search model. 098 * @param searchModel The reference search model. 099 * @return a ContentSearcher backed by the given search model. 100 */ 101 public SearchModelContentSearcher create(SearchModel searchModel) 102 { 103 return new SearchModelContentSearcher(searchModel); 104 } 105 106 /** 107 * Create a simple ContentSearcher from a list of content types. 108 * @param contentTypes The content types to search on. 109 * @return a ContentSearcher referencing the given content types. 110 */ 111 public SimpleContentSearcher create(String... contentTypes) 112 { 113 return new SimpleContentSearcher(Arrays.asList(contentTypes)); 114 } 115 116 /** 117 * Create a simple ContentSearcher from a list of content types. 118 * @param contentTypes The content types to search on. 119 * @return a ContentSearcher referencing the given content types. 120 */ 121 public SimpleContentSearcher create(Collection<String> contentTypes) 122 { 123 return new SimpleContentSearcher(contentTypes); 124 } 125 126 /** 127 * A ContentSearcher backed by a {@link SearchModel}. 128 */ 129 public class SearchModelContentSearcher 130 { 131 private SearchUIModel _searchModel; 132 private List<Sort> _sort; 133 private String _searchMode; 134 private int _start; 135 private int _maxResults; 136 private boolean _checkRights; 137 138 /** 139 * Build a ContentSearcher referencing a {@link SearchModel}. 140 * @param searchModel the {@link SearchModel}. 141 */ 142 public SearchModelContentSearcher(SearchModel searchModel) 143 { 144 // TODO Do not cast. 145 this._searchModel = (SearchUIModel) searchModel; 146 this._sort = new ArrayList<>(); 147 this._searchMode = "simple"; 148 this._start = 0; 149 this._maxResults = Integer.MAX_VALUE; 150 this._checkRights = true; 151 } 152 153 /** 154 * Add a sort criterion. 155 * @param fieldRef The field reference (name of a SearchField). 156 * @param order The sort order. 157 * @return The ContentSearcher itself. 158 */ 159 public SearchModelContentSearcher addSort(String fieldRef, Order order) 160 { 161 _sort.add(new Sort(fieldRef, order)); 162 return this; 163 } 164 165 /** 166 * Set the sort criteria. 167 * @param sortCriteria The sort criteria as a List. 168 * @return The ContentSearcher itself. 169 */ 170 public SearchModelContentSearcher withSort(List<Sort> sortCriteria) 171 { 172 _sort = new ArrayList<>(sortCriteria); 173 return this; 174 } 175 176 /** 177 * Set the search mode. 178 * @param searchMode The search mode. 179 * @return The ContentSearcher itself. 180 */ 181 public SearchModelContentSearcher withSearchMode(String searchMode) 182 { 183 _searchMode = searchMode; 184 return this; 185 } 186 187 /** 188 * Set the limits to use. 189 * @param start The start index. 190 * @param maxResults The maximum number of results. 191 * @return The ContentSearcher itself. 192 */ 193 public SearchModelContentSearcher withLimits(int start, int maxResults) 194 { 195 this._start = start; 196 this._maxResults = maxResults; 197 return this; 198 } 199 200 /** 201 * Whether to check rights when searching, false otherwise. 202 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 203 * @return The ContentSearcher itself. 204 */ 205 public SearchModelContentSearcher setCheckRights(boolean checkRights) 206 { 207 _checkRights = checkRights; 208 return this; 209 } 210 211 /** 212 * Search the contents. 213 * @param values The values for search criteria defined in the model. 214 * @param <C> The type Content 215 * @return The search results as {@link AmetysObject}s. 216 * @throws Exception if an error occurs. 217 */ 218 public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception 219 { 220 return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search(); 221 } 222 223 /** 224 * Search the contents. 225 * @param values The values for search criteria defined in the model. 226 * @param <C> The type Content * 227 * @return The search results. 228 * @throws Exception if an error occurs. 229 */ 230 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception 231 { 232 return searchWithFacets(values, Collections.emptyMap()); 233 } 234 235 /** 236 * Search the contents. 237 * @param <C> The type Content 238 * @param values The values for search criteria defined in the model. 239 * @param contextualParameters The search contextual parameters. 240 * @return The search results. 241 * @throws Exception if an error occurs. 242 */ 243 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception 244 { 245 return searchWithFacets(values, Collections.emptyMap(), contextualParameters); 246 } 247 248 /** 249 * Search the contents. 250 * @param <C> The type Content 251 * @param values The values for search criteria defined in the model. 252 * @param facetValues The facet values, indexed 253 * @param contextualParameters The search contextual parameters. 254 * @return The search results. 255 * @throws Exception if an error occurs. 256 */ 257 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception 258 { 259 return _searcher(values, facetValues, contextualParameters).searchWithFacets(); 260 } 261 262 private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) 263 { 264 Query query = _queryBuilder.build(_searchModel, _searchMode, values, contextualParameters); 265 266 List<Sort> sort = getSort(contextualParameters); 267 List<SearchField> facets = getFacets(contextualParameters); 268 269 return _searcherFactory.create() 270 .withQuery(query) 271 .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 272 .withSort(sort) 273 .withFacets(facets) 274 .withFacetValues(facetValues) 275 .withLimits(_start, _maxResults) 276 .addContextElement(AccessSearchFilter.OBJECT_TYPE, AccessSearchFilter.TYPE_CONTENT) 277 .setCheckRights(_checkRights); 278 } 279 280 /** 281 * Get the sort criteria. 282 * @param contextualParameters The search contextual parameters. 283 * @return The sort criteria. 284 */ 285 protected List<Sort> getSort(Map<String, Object> contextualParameters) 286 { 287 List<Sort> sort = new ArrayList<>(); 288 289 if (!_sort.isEmpty()) 290 { 291 // Index criterion and results by search field name. 292 Map<String, SearchCriterion> criteriaByName = new HashMap<>(); 293 for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values()) 294 { 295 if (criterion.getSearchField() != null) 296 { 297 criteriaByName.put(criterion.getSearchField().getName(), criterion); 298 } 299 } 300 Map<String, ResultField> resultsByName = new HashMap<>(); 301 for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values()) 302 { 303 if (resultField.getSearchField() != null) 304 { 305 resultsByName.put(resultField.getSearchField().getName(), resultField); 306 } 307 } 308 309 for (Sort sortCriterion : _sort) 310 { 311 String id = sortCriterion.getField(); 312 313 SearchField searchField = null; 314 if (criteriaByName.containsKey(id)) 315 { 316 searchField = criteriaByName.get(id).getSearchField(); 317 } 318 else if (resultsByName.containsKey(id)) 319 { 320 searchField = resultsByName.get(id).getSearchField(); 321 } 322 323 if (searchField != null) 324 { 325 sort.add(new Sort(searchField, sortCriterion.getOrder())); 326 } 327 else 328 { 329 throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model."); 330 } 331 } 332 } 333 else 334 { 335 // Get the default sort from the search model. 336 } 337 338 return sort; 339 } 340 341 /** 342 * Get the facet fields. 343 * @param contextualParameters The search contextual parameters. 344 * @return The facet fields as a List. 345 */ 346 protected List<SearchField> getFacets(Map<String, Object> contextualParameters) 347 { 348 List<SearchField> facets = new ArrayList<>(); 349 350 for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values()) 351 { 352 if (criterion.getSearchField() != null) 353 { 354 facets.add(criterion.getSearchField()); 355 } 356 } 357 358 return facets; 359 } 360 361 } 362 363 /** 364 * A ContentSearcher on a list of content types. 365 */ 366 public class SimpleContentSearcher 367 { 368 369 private Set<String> _contentTypes; 370 private List<Sort> _sort; 371 private List<String> _facets; 372 private int _start; 373 private int _maxResults; 374 private boolean _checkRights; 375 376 /** 377 * Build a content searcher on a list of content types. 378 * @param contentTypes A collection of content types to search on. 379 */ 380 public SimpleContentSearcher(Collection<String> contentTypes) 381 { 382 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 383 this._sort = new ArrayList<>(); 384 this._facets = new ArrayList<>(); 385 this._start = 0; 386 this._maxResults = Integer.MAX_VALUE; 387 this._checkRights = true; 388 } 389 390 /** 391 * Set the sort criteria. 392 * @param sortCriteria The sort criteria as a List. 393 * @return The ContentSearcher itself. 394 */ 395 public SimpleContentSearcher withSort(List<Sort> sortCriteria) 396 { 397 _sort = new ArrayList<>(sortCriteria); 398 return this; 399 } 400 401 /** 402 * Add a sort criterion. 403 * @param fieldRef The field reference (name of a SearchField). 404 * @param order The sort order. 405 * @return The ContentSearcher itself. 406 */ 407 public SimpleContentSearcher addSort(String fieldRef, Order order) 408 { 409 _sort.add(new Sort(fieldRef, order)); 410 return this; 411 } 412 413 /** 414 * Set the facets. 415 * @param facets The facets list. 416 * @return The ContentSearcher itself. 417 */ 418 public SimpleContentSearcher withFacets(List<String> facets) 419 { 420 _facets = new ArrayList<>(facets); 421 return this; 422 } 423 424 /** 425 * Set the facets. 426 * @param facets The facets list. 427 * @return The ContentSearcher itself. 428 */ 429 public SimpleContentSearcher withFacets(String... facets) 430 { 431 _facets = Arrays.asList(facets); 432 return this; 433 } 434 435 /** 436 * Set the limits to use. 437 * @param start The start index. 438 * @param maxResults The maximum number of results. 439 * @return The ContentSearcher itself. 440 */ 441 public SimpleContentSearcher withLimits(int start, int maxResults) 442 { 443 this._start = start; 444 this._maxResults = maxResults; 445 return this; 446 } 447 448 /** 449 * Whether to check rights when searching, false otherwise. 450 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 451 * @return The ContentSearcher itself. 452 */ 453 public SimpleContentSearcher setCheckRights(boolean checkRights) 454 { 455 _checkRights = checkRights; 456 return this; 457 } 458 459 /** 460 * Search the contents. 461 * @param <C> The type Content 462 * @param query The query to execute. 463 * @return The search results as {@link AmetysObject}s. 464 * @throws Exception if an error occurs. 465 */ 466 public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception 467 { 468 return _searcher(query, Collections.emptyMap()).search(); 469 } 470 471 /** 472 * Search the contents. 473 * @param <C> The type Content 474 * @param query The query to execute. 475 * @return The search results. 476 * @throws Exception if an error occurs. 477 */ 478 public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception 479 { 480 return searchWithFacets(query, Collections.emptyMap()); 481 } 482 483 /** 484 * Search the contents. 485 * @param <C> The type Content 486 * @param query The query to execute. 487 * @param facetValues The facet values. 488 * @return The search results. 489 * @throws Exception if an error occurs. 490 */ 491 public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception 492 { 493 return _searcher(query, facetValues).searchWithFacets(); 494 } 495 496 private Searcher _searcher(Query query, Map<String, List<String>> facetValues) 497 { 498 List<Sort> sort = getSort(); 499 List<SearchField> facets = getFacets(); 500 501 List<Query> queries = new ArrayList<>(); 502 queries.add(query); 503 504 List<Query> filterQueries = new ArrayList<>(); 505 filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 506 507 if (!_contentTypes.isEmpty()) 508 { 509 filterQueries.add(new ContentTypeQuery(_contentTypes)); 510 } 511 512 return _searcherFactory.create() 513 .withQuery(query) 514 .withFilterQueries(filterQueries) 515 .withSort(sort) 516 .withFacets(facets) 517 .withFacetValues(facetValues) 518 .withLimits(_start, _maxResults) 519 .addContextElement(AccessSearchFilter.OBJECT_TYPE, AccessSearchFilter.TYPE_CONTENT) 520 .setCheckRights(_checkRights); 521 } 522 523 /** 524 * Get the sort criteria from the specified field names. 525 * @return The sort criteria. 526 */ 527 protected List<Sort> getSort() 528 { 529 List<Sort> sortCriteria = new ArrayList<>(); 530 531 for (Sort sort : _sort) 532 { 533 String fieldName = sort.getField(); 534 Order order = sort.getOrder(); 535 536 SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 537// SearchField searchField = getSearchField(_contentTypes, fieldName); 538 if (searchField != null) 539 { 540 sortCriteria.add(new Sort(searchField, order)); 541 } 542 } 543 544 return sortCriteria; 545 } 546 547 /** 548 * Get the facet criteria as a list of SearchField from the specified field names. 549 * @return The facets as a List of SearchField. 550 */ 551 protected List<SearchField> getFacets() 552 { 553 List<SearchField> facets = new ArrayList<>(); 554 555 for (String fieldName : _facets) 556 { 557 SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 558// SearchField searchField = getSearchField(_contentTypes, fieldName); 559 if (searchField != null) 560 { 561 facets.add(searchField); 562 } 563 } 564 565 return facets; 566 } 567 568// /** 569// * Get a {@link SearchField} from a field name. 570// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 571// * @return The {@link SearchField} corresponding to the 572// */ 573// public SearchField getSearchField(Collection<String> contentTypes, String fieldName) 574// { 575// SearchField searchField = null; 576// 577// if (_sysPropEP.hasExtension(fieldName)) 578// { 579// SystemProperty property = _sysPropEP.getExtension(fieldName); 580// searchField = property.getSearchField(); 581// } 582// else 583// { 584// String metadataPath = fieldName.replace('.', '/'); 585// _searchHelper.getMetadataSearchField(contentTypes, metadataPath); 586// } 587// 588// return searchField; 589// } 590 591// /** 592// * Get a {@link SearchField} from a field name. 593// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 594// * @return The {@link SearchField} corresponding to the 595// */ 596// protected SearchField getSearchField(String fieldName) 597// { 598// SearchField searchField = null; 599// 600// if (_sysPropEP.hasExtension(fieldName)) 601// { 602// SystemProperty property = _sysPropEP.getExtension(fieldName); 603// searchField = property.getSearchField(); 604// } 605// else 606// { 607// String metaPath = fieldName.replace('.', '/'); 608// if (_contentTypeId != null) 609// { 610// ContentType cType = _cTypeEP.getExtension(_contentTypeId); 611// List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionsByPath(cType, metaPath); 612// 613// boolean joinedMetadata = isJoinedMetadata(metaDefs); 614// 615// if (!joinedMetadata) 616// { 617// MetadataType type = metaDefs.get(metaDefs.size() - 1).getType(); 618// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, type); 619// } 620// else 621// { 622// throw new IllegalArgumentException("The metadata '" + fieldName + "' can't be used as it is joined."); 623// } 624// } 625// else if (fieldName.equals("title")) 626// { 627// // No specific content type: allow only title. 628// // TODO Provide a standard "title" metadata definition to avoid getting it on a random content type. 629// ContentType cType = _cTypeEP.getExtension(_contentTypes.iterator().next()); 630// MetadataDefinition metaDef = cType.getMetadataDefinition("title"); 631// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, metaDef.getType()); 632// } 633// } 634// 635// if (searchField == null) 636// { 637// throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the selected content types."); 638// } 639// 640// return searchField; 641// } 642// 643// /** 644// * Test if a metadata, represented by a list of successive definitions, is joined. 645// * @param metaDefs The list of successive definitions. 646// * @return <code>true</code> if the metadata is joined, <code>false</code> otherwise. 647// */ 648// protected boolean isJoinedMetadata(List<MetadataDefinition> metaDefs) 649// { 650// boolean joinedMetadata = false; 651// Iterator<MetadataDefinition> metaDefIt = metaDefs.iterator(); 652// while (metaDefIt.hasNext()) 653// { 654// MetadataType type = metaDefIt.next().getType(); 655// // The column represents a "joined" value if it has a content metadata (except if it's the last one). 656// if ((type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) && metaDefIt.hasNext()) 657// { 658// joinedMetadata = true; 659// } 660// } 661// return joinedMetadata; 662// } 663 664 } 665 666}