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