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.SearchCriterion; 044import org.ametys.cms.search.model.SearchModel; 045import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 046import org.ametys.cms.search.query.DocumentTypeQuery; 047import org.ametys.cms.search.query.Query; 048import org.ametys.cms.search.solr.SearcherFactory; 049import org.ametys.cms.search.solr.SearcherFactory.Searcher; 050import org.ametys.plugins.repository.AmetysObject; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.runtime.model.ModelViewItem; 053import org.ametys.runtime.model.ViewItem; 054import org.ametys.runtime.model.ViewItemAccessor; 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 SearchModel _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 this._searchModel = 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 313 Map<String, SearchField> resultSearchFields = _getResultSearchFields(_searchModel.getResultItems(contextualParameters)); 314 315 for (Sort sortCriterion : _sort) 316 { 317 String id = sortCriterion.getField(); 318 319 SearchField searchField = null; 320 if (criteriaByName.containsKey(id)) 321 { 322 searchField = criteriaByName.get(id).getSearchField(); 323 } 324 else if (resultSearchFields.containsKey(id)) 325 { 326 searchField = resultSearchFields.get(id); 327 } 328 329 if (searchField == null) 330 { 331 throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model."); 332 } 333 else if (searchField.getSortField() == null) 334 { 335 getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id); 336 } 337 else 338 { 339 sort.add(new Sort(searchField, sortCriterion.getOrder())); 340 } 341 } 342 } 343 else 344 { 345 // Get the default sort from the search model. 346 } 347 348 return sort; 349 } 350 351 private Map<String, SearchField> _getResultSearchFields(ViewItemAccessor viewItemAccessor) 352 { 353 Map<String, SearchField> searchFields = new HashMap<>(); 354 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 355 { 356 if (viewItem instanceof ViewItemAccessor itemAccessor && !itemAccessor.getViewItems().isEmpty()) 357 { 358 searchFields.putAll(_getResultSearchFields(itemAccessor)); 359 } 360 else if (viewItem instanceof ModelViewItem modelViewItem) 361 { 362 // this item is a leaf, add its search field to the results map 363 SearchField searchField = _searchHelper.getSearchField(modelViewItem); 364 if (searchField != null) 365 { 366 searchFields.put(searchField.getName(), searchField); 367 } 368 } 369 } 370 371 return searchFields; 372 } 373 374 /** 375 * Get the facet fields. 376 * @param contextualParameters The search contextual parameters. 377 * @return The facet fields as a List. 378 */ 379 protected List<SearchField> getFacets(Map<String, Object> contextualParameters) 380 { 381 List<SearchField> facets = new ArrayList<>(); 382 383 for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values()) 384 { 385 if (criterion.getSearchField() != null) 386 { 387 facets.add(criterion.getSearchField()); 388 } 389 } 390 391 return facets; 392 } 393 394 } 395 396 /** 397 * A ContentSearcher on a list of content types. 398 */ 399 public class SimpleContentSearcher 400 { 401 402 private Set<String> _contentTypes; 403 private List<Sort> _sort; 404 private List<String> _facets; 405 private int _start; 406 private int _maxResults; 407 private boolean _checkRights; 408 private List<String> _filterQueryStrings; 409 private List<Query> _filterQueries; 410 411 /** 412 * Build a content searcher on a list of content types. 413 * @param contentTypes A collection of content types to search on. 414 */ 415 public SimpleContentSearcher(Collection<String> contentTypes) 416 { 417 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 418 this._sort = new ArrayList<>(); 419 this._facets = new ArrayList<>(); 420 this._start = 0; 421 this._maxResults = Integer.MAX_VALUE; 422 this._checkRights = true; 423 } 424 425 /** 426 * Set the filter queries. 427 * @param filterQueries the filter queries. 428 * @return The ContentSearcher itself. 429 */ 430 public SimpleContentSearcher withFilterQueries(List<Query> filterQueries) 431 { 432 _filterQueries = filterQueries; 433 return this; 434 } 435 436 /** 437 * Set the filter queries. 438 * @param filterQueryStrings the filter queries. 439 * @return The ContentSearcher itself. 440 */ 441 public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings) 442 { 443 _filterQueryStrings = filterQueryStrings; 444 return this; 445 } 446 447 /** 448 * Set the sort criteria. 449 * @param sortCriteria The sort criteria as a List. 450 * @return The ContentSearcher itself. 451 */ 452 public SimpleContentSearcher withSort(List<Sort> sortCriteria) 453 { 454 _sort = new ArrayList<>(sortCriteria); 455 return this; 456 } 457 458 /** 459 * Add a sort criterion. 460 * @param fieldRef The field reference (name of a SearchField). 461 * @param order The sort order. 462 * @return The ContentSearcher itself. 463 */ 464 public SimpleContentSearcher addSort(String fieldRef, Order order) 465 { 466 _sort.add(new Sort(fieldRef, order)); 467 return this; 468 } 469 470 /** 471 * Set the facets. 472 * @param facets The facets list. 473 * @return The ContentSearcher itself. 474 */ 475 public SimpleContentSearcher withFacets(Collection<String> facets) 476 { 477 _facets = new ArrayList<>(facets); 478 return this; 479 } 480 481 /** 482 * Set the facets. 483 * @param facets The facets list. 484 * @return The ContentSearcher itself. 485 */ 486 public SimpleContentSearcher withFacets(String... facets) 487 { 488 _facets = Arrays.asList(facets); 489 return this; 490 } 491 492 /** 493 * Set the limits to use. 494 * @param start The start index. 495 * @param maxResults The maximum number of results. 496 * @return The ContentSearcher itself. 497 */ 498 public SimpleContentSearcher withLimits(int start, int maxResults) 499 { 500 this._start = start; 501 this._maxResults = maxResults; 502 return this; 503 } 504 505 /** 506 * Whether to check rights when searching, false otherwise. 507 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 508 * @return The ContentSearcher itself. 509 */ 510 public SimpleContentSearcher setCheckRights(boolean checkRights) 511 { 512 _checkRights = checkRights; 513 return this; 514 } 515 516 /** 517 * Search the contents. 518 * @param <C> The type Content 519 * @param query The query object to execute. 520 * @return The search results as {@link AmetysObject}s. 521 * @throws Exception if an error occurs. 522 */ 523 public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception 524 { 525 return _searcher(query, Collections.emptyMap()).search(); 526 } 527 528 /** 529 * Search the contents. 530 * @param <C> The type Content 531 * @param query The query string to execute. 532 * @return The search results as {@link AmetysObject}s. 533 * @throws Exception if an error occurs. 534 */ 535 public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception 536 { 537 return _searcher(query, Collections.emptyMap()).search(); 538 } 539 540 /** 541 * Search the contents. 542 * @param <C> The type Content 543 * @param query The query objet to execute. 544 * @return The search results. 545 * @throws Exception if an error occurs. 546 */ 547 public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception 548 { 549 return searchWithFacets(query, Collections.emptyMap()); 550 } 551 552 /** 553 * Search the contents. 554 * @param <C> The type Content 555 * @param query The query string to execute. 556 * @return The search results. 557 * @throws Exception if an error occurs. 558 */ 559 public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception 560 { 561 return searchWithFacets(query, Collections.emptyMap()); 562 } 563 564 /** 565 * Search the contents. 566 * @param <C> The type Content 567 * @param query The query object to execute. 568 * @param facetValues The facet values. 569 * @return The search results. 570 * @throws Exception if an error occurs. 571 */ 572 public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception 573 { 574 return _searcher(query, facetValues).searchWithFacets(); 575 } 576 577 /** 578 * Search the contents. 579 * @param <C> The type Content 580 * @param query The query string to execute. 581 * @param facetValues The facet values. 582 * @return The search results. 583 * @throws Exception if an error occurs. 584 */ 585 public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception 586 { 587 return _searcher(query, facetValues).searchWithFacets(); 588 } 589 590 private Searcher _searcher(String query, Map<String, List<String>> facetValues) 591 { 592 return _searcher(facetValues).withQueryString(query); 593 } 594 595 private Searcher _searcher(Query query, Map<String, List<String>> facetValues) 596 { 597 return _searcher(facetValues).withQuery(query); 598 } 599 600 private Searcher _searcher(Map<String, List<String>> facetValues) 601 { 602 List<Sort> sort = getSort(); 603 List<SearchField> facets = getFacets(); 604 605 List<Query> filterQueries = new ArrayList<>(); 606 filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 607 608 if (!_contentTypes.isEmpty()) 609 { 610 filterQueries.add(_queryBuilder.createContentTypeOrMixinQuery(_contentTypes, null, true)); 611 } 612 613 if (_filterQueries != null) 614 { 615 filterQueries.addAll(_filterQueries); 616 } 617 618 List<String> filterQueryStrings = new ArrayList<>(); 619 620 if (_filterQueryStrings != null) 621 { 622 filterQueryStrings.addAll(_filterQueryStrings); 623 } 624 625 return _searcherFactory.create() 626 .withFilterQueries(filterQueries) 627 .withFilterQueryStrings(filterQueryStrings) 628 .withSort(sort) 629 .withFacets(facets) 630 .withFacetValues(facetValues) 631 .withLimits(_start, _maxResults) 632 .setCheckRights(_checkRights); 633 } 634 635 /** 636 * Get the sort criteria from the specified field names. 637 * @return The sort criteria. 638 */ 639 protected List<Sort> getSort() 640 { 641 List<Sort> sortCriteria = new ArrayList<>(); 642 643 for (Sort sort : _sort) 644 { 645 String fieldName = sort.getField(); 646 Order order = sort.getOrder(); 647 648 Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 649// SearchField searchField = getSearchField(_contentTypes, fieldName); 650 if (searchField.isPresent()) 651 { 652 sortCriteria.add(new Sort(searchField.get(), order)); 653 } 654 else 655 { 656 throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName)); 657 } 658 } 659 660 return sortCriteria; 661 } 662 663 /** 664 * Get the facet criteria as a list of SearchField from the specified field names. 665 * @return The facets as a List of SearchField. 666 */ 667 protected List<SearchField> getFacets() 668 { 669 List<SearchField> facets = new ArrayList<>(); 670 671 for (String fieldName : _facets) 672 { 673 Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 674 if (searchField.isPresent()) 675 { 676 facets.add(searchField.get()); 677 } 678 else 679 { 680 throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName)); 681 } 682 } 683 684 return facets; 685 } 686 687 private String _exceptionMessageForEmptySearchField(String fieldName) 688 { 689 return "The field '" + fieldName + "' can't be found in the selected content types."; 690 } 691 } 692}