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.model.ResultField; 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.ContentTypeQuery; 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 <C> The type Content * 226 * @return The search results. 227 * @throws Exception if an error occurs. 228 */ 229 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception 230 { 231 return searchWithFacets(values, Collections.emptyMap()); 232 } 233 234 /** 235 * Search the contents. 236 * @param <C> The type Content 237 * @param values The values for search criteria defined in the model. 238 * @param contextualParameters The search contextual parameters. 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, Map<String, Object> contextualParameters) throws Exception 243 { 244 return searchWithFacets(values, Collections.emptyMap(), contextualParameters); 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 facetValues The facet values, indexed 252 * @param contextualParameters The search contextual parameters. 253 * @return The search results. 254 * @throws Exception if an error occurs. 255 */ 256 public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception 257 { 258 return _searcher(values, facetValues, contextualParameters).searchWithFacets(); 259 } 260 261 private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) 262 { 263 Query query = _queryBuilder.build(_searchModel, _searchMode, true, values, contextualParameters); 264 265 List<Sort> sort = getSort(contextualParameters); 266 List<SearchField> facets = getFacets(contextualParameters); 267 268 return _searcherFactory.create() 269 .withQuery(query) 270 .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 271 .withSort(sort) 272 .withFacets(facets) 273 .withFacetValues(facetValues) 274 .withLimits(_start, _maxResults) 275 .setCheckRights(_checkRights); 276 } 277 278 /** 279 * Get the sort criteria. 280 * @param contextualParameters The search contextual parameters. 281 * @return The sort criteria. 282 */ 283 @SuppressWarnings("synthetic-access") 284 protected List<Sort> getSort(Map<String, Object> contextualParameters) 285 { 286 List<Sort> sort = new ArrayList<>(); 287 288 if (!_sort.isEmpty()) 289 { 290 // Index criterion and results by search field name. 291 Map<String, SearchCriterion> criteriaByName = new HashMap<>(); 292 for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values()) 293 { 294 if (criterion.getSearchField() != null) 295 { 296 criteriaByName.put(criterion.getSearchField().getName(), criterion); 297 } 298 } 299 Map<String, ResultField> resultsByName = new HashMap<>(); 300 for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values()) 301 { 302 if (resultField.getSearchField() != null) 303 { 304 resultsByName.put(resultField.getSearchField().getName(), resultField); 305 } 306 } 307 308 for (Sort sortCriterion : _sort) 309 { 310 String id = sortCriterion.getField(); 311 312 SearchField searchField = null; 313 if (criteriaByName.containsKey(id)) 314 { 315 searchField = criteriaByName.get(id).getSearchField(); 316 } 317 else if (resultsByName.containsKey(id)) 318 { 319 searchField = resultsByName.get(id).getSearchField(); 320 } 321 322 if (searchField == null) 323 { 324 throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model."); 325 } 326 else if (searchField.getSortField() == null) 327 { 328 getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id); 329 } 330 else 331 { 332 sort.add(new Sort(searchField, sortCriterion.getOrder())); 333 } 334 } 335 } 336 else 337 { 338 // Get the default sort from the search model. 339 } 340 341 return sort; 342 } 343 344 /** 345 * Get the facet fields. 346 * @param contextualParameters The search contextual parameters. 347 * @return The facet fields as a List. 348 */ 349 protected List<SearchField> getFacets(Map<String, Object> contextualParameters) 350 { 351 List<SearchField> facets = new ArrayList<>(); 352 353 for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values()) 354 { 355 if (criterion.getSearchField() != null) 356 { 357 facets.add(criterion.getSearchField()); 358 } 359 } 360 361 return facets; 362 } 363 364 } 365 366 /** 367 * A ContentSearcher on a list of content types. 368 */ 369 public class SimpleContentSearcher 370 { 371 372 private Set<String> _contentTypes; 373 private List<Sort> _sort; 374 private List<String> _facets; 375 private int _start; 376 private int _maxResults; 377 private boolean _checkRights; 378 private List<String> _filterQueryStrings; 379 private List<Query> _filterQueries; 380 381 /** 382 * Build a content searcher on a list of content types. 383 * @param contentTypes A collection of content types to search on. 384 */ 385 public SimpleContentSearcher(Collection<String> contentTypes) 386 { 387 this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet(); 388 this._sort = new ArrayList<>(); 389 this._facets = new ArrayList<>(); 390 this._start = 0; 391 this._maxResults = Integer.MAX_VALUE; 392 this._checkRights = true; 393 } 394 395 /** 396 * Set the filter queries. 397 * @param filterQueries the filter queries. 398 * @return The ContentSearcher itself. 399 */ 400 public SimpleContentSearcher withFilterQueries(List<Query> filterQueries) 401 { 402 _filterQueries = filterQueries; 403 return this; 404 } 405 406 /** 407 * Set the filter queries. 408 * @param filterQueryStrings the filter queries. 409 * @return The ContentSearcher itself. 410 */ 411 public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings) 412 { 413 _filterQueryStrings = filterQueryStrings; 414 return this; 415 } 416 417 /** 418 * Set the sort criteria. 419 * @param sortCriteria The sort criteria as a List. 420 * @return The ContentSearcher itself. 421 */ 422 public SimpleContentSearcher withSort(List<Sort> sortCriteria) 423 { 424 _sort = new ArrayList<>(sortCriteria); 425 return this; 426 } 427 428 /** 429 * Add a sort criterion. 430 * @param fieldRef The field reference (name of a SearchField). 431 * @param order The sort order. 432 * @return The ContentSearcher itself. 433 */ 434 public SimpleContentSearcher addSort(String fieldRef, Order order) 435 { 436 _sort.add(new Sort(fieldRef, order)); 437 return this; 438 } 439 440 /** 441 * Set the facets. 442 * @param facets The facets list. 443 * @return The ContentSearcher itself. 444 */ 445 public SimpleContentSearcher withFacets(Collection<String> facets) 446 { 447 _facets = new ArrayList<>(facets); 448 return this; 449 } 450 451 /** 452 * Set the facets. 453 * @param facets The facets list. 454 * @return The ContentSearcher itself. 455 */ 456 public SimpleContentSearcher withFacets(String... facets) 457 { 458 _facets = Arrays.asList(facets); 459 return this; 460 } 461 462 /** 463 * Set the limits to use. 464 * @param start The start index. 465 * @param maxResults The maximum number of results. 466 * @return The ContentSearcher itself. 467 */ 468 public SimpleContentSearcher withLimits(int start, int maxResults) 469 { 470 this._start = start; 471 this._maxResults = maxResults; 472 return this; 473 } 474 475 /** 476 * Whether to check rights when searching, false otherwise. 477 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 478 * @return The ContentSearcher itself. 479 */ 480 public SimpleContentSearcher setCheckRights(boolean checkRights) 481 { 482 _checkRights = checkRights; 483 return this; 484 } 485 486 /** 487 * Search the contents. 488 * @param <C> The type Content 489 * @param query The query object to execute. 490 * @return The search results as {@link AmetysObject}s. 491 * @throws Exception if an error occurs. 492 */ 493 public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception 494 { 495 return _searcher(query, Collections.emptyMap()).search(); 496 } 497 498 /** 499 * Search the contents. 500 * @param <C> The type Content 501 * @param query The query string to execute. 502 * @return The search results as {@link AmetysObject}s. 503 * @throws Exception if an error occurs. 504 */ 505 public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception 506 { 507 return _searcher(query, Collections.emptyMap()).search(); 508 } 509 510 /** 511 * Search the contents. 512 * @param <C> The type Content 513 * @param query The query objet to execute. 514 * @return The search results. 515 * @throws Exception if an error occurs. 516 */ 517 public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception 518 { 519 return searchWithFacets(query, Collections.emptyMap()); 520 } 521 522 /** 523 * Search the contents. 524 * @param <C> The type Content 525 * @param query The query string to execute. 526 * @return The search results. 527 * @throws Exception if an error occurs. 528 */ 529 public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception 530 { 531 return searchWithFacets(query, Collections.emptyMap()); 532 } 533 534 /** 535 * Search the contents. 536 * @param <C> The type Content 537 * @param query The query object to execute. 538 * @param facetValues The facet values. 539 * @return The search results. 540 * @throws Exception if an error occurs. 541 */ 542 public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception 543 { 544 return _searcher(query, facetValues).searchWithFacets(); 545 } 546 547 /** 548 * Search the contents. 549 * @param <C> The type Content 550 * @param query The query string 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(String query, Map<String, List<String>> facetValues) throws Exception 556 { 557 return _searcher(query, facetValues).searchWithFacets(); 558 } 559 560 private Searcher _searcher(String query, Map<String, List<String>> facetValues) 561 { 562 return _searcher(facetValues).withQueryString(query); 563 } 564 565 private Searcher _searcher(Query query, Map<String, List<String>> facetValues) 566 { 567 return _searcher(facetValues).withQuery(query); 568 } 569 570 private Searcher _searcher(Map<String, List<String>> facetValues) 571 { 572 List<Sort> sort = getSort(); 573 List<SearchField> facets = getFacets(); 574 575 List<Query> filterQueries = new ArrayList<>(); 576 filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)); 577 578 if (!_contentTypes.isEmpty()) 579 { 580 filterQueries.add(new ContentTypeQuery(_contentTypes)); 581 } 582 583 if (_filterQueries != null) 584 { 585 filterQueries.addAll(_filterQueries); 586 } 587 588 List<String> filterQueryStrings = new ArrayList<>(); 589 590 if (_filterQueryStrings != null) 591 { 592 filterQueryStrings.addAll(_filterQueryStrings); 593 } 594 595 return _searcherFactory.create() 596 .withFilterQueries(filterQueries) 597 .withFilterQueryStrings(filterQueryStrings) 598 .withSort(sort) 599 .withFacets(facets) 600 .withFacetValues(facetValues) 601 .withLimits(_start, _maxResults) 602 .setCheckRights(_checkRights); 603 } 604 605 /** 606 * Get the sort criteria from the specified field names. 607 * @return The sort criteria. 608 */ 609 protected List<Sort> getSort() 610 { 611 List<Sort> sortCriteria = new ArrayList<>(); 612 613 for (Sort sort : _sort) 614 { 615 String fieldName = sort.getField(); 616 Order order = sort.getOrder(); 617 618 SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 619// SearchField searchField = getSearchField(_contentTypes, fieldName); 620 if (searchField != null) 621 { 622 sortCriteria.add(new Sort(searchField, order)); 623 } 624 } 625 626 return sortCriteria; 627 } 628 629 /** 630 * Get the facet criteria as a list of SearchField from the specified field names. 631 * @return The facets as a List of SearchField. 632 */ 633 protected List<SearchField> getFacets() 634 { 635 List<SearchField> facets = new ArrayList<>(); 636 637 for (String fieldName : _facets) 638 { 639 SearchField searchField = _searchHelper.getSearchField(_contentTypes, fieldName); 640// SearchField searchField = getSearchField(_contentTypes, fieldName); 641 if (searchField != null) 642 { 643 facets.add(searchField); 644 } 645 } 646 647 return facets; 648 } 649 650// /** 651// * Get a {@link SearchField} from a field name. 652// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 653// * @return The {@link SearchField} corresponding to the 654// */ 655// public SearchField getSearchField(Collection<String> contentTypes, String fieldName) 656// { 657// SearchField searchField = null; 658// 659// if (_sysPropEP.hasExtension(fieldName)) 660// { 661// SystemProperty property = _sysPropEP.getExtension(fieldName); 662// searchField = property.getSearchField(); 663// } 664// else 665// { 666// String metadataPath = fieldName.replace('.', '/'); 667// _searchHelper.getMetadataSearchField(contentTypes, metadataPath); 668// } 669// 670// return searchField; 671// } 672 673// /** 674// * Get a {@link SearchField} from a field name. 675// * @param fieldName The field name, can be either a system property ID or a metadata path (not joined). 676// * @return The {@link SearchField} corresponding to the 677// */ 678// protected SearchField getSearchField(String fieldName) 679// { 680// SearchField searchField = null; 681// 682// if (_sysPropEP.hasExtension(fieldName)) 683// { 684// SystemProperty property = _sysPropEP.getExtension(fieldName); 685// searchField = property.getSearchField(); 686// } 687// else 688// { 689// String metaPath = fieldName.replace('.', '/'); 690// if (_contentTypeId != null) 691// { 692// ContentType cType = _cTypeEP.getExtension(_contentTypeId); 693// List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionsByPath(cType, metaPath); 694// 695// boolean joinedMetadata = isJoinedMetadata(metaDefs); 696// 697// if (!joinedMetadata) 698// { 699// MetadataType type = metaDefs.get(metaDefs.size() - 1).getType(); 700// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, type); 701// } 702// else 703// { 704// throw new IllegalArgumentException("The metadata '" + fieldName + "' can't be used as it is joined."); 705// } 706// } 707// else if (fieldName.equals("title")) 708// { 709// // No specific content type: allow only title. 710// // TODO Provide a standard "title" metadata definition to avoid getting it on a random content type. 711// ContentType cType = _cTypeEP.getExtension(_contentTypes.iterator().next()); 712// MetadataDefinition metaDef = cType.getMetadataDefinition("title"); 713// searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, metaDef.getType()); 714// } 715// } 716// 717// if (searchField == null) 718// { 719// throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the selected content types."); 720// } 721// 722// return searchField; 723// } 724// 725// /** 726// * Test if a metadata, represented by a list of successive definitions, is joined. 727// * @param metaDefs The list of successive definitions. 728// * @return <code>true</code> if the metadata is joined, <code>false</code> otherwise. 729// */ 730// protected boolean isJoinedMetadata(List<MetadataDefinition> metaDefs) 731// { 732// boolean joinedMetadata = false; 733// Iterator<MetadataDefinition> metaDefIt = metaDefs.iterator(); 734// while (metaDefIt.hasNext()) 735// { 736// MetadataType type = metaDefIt.next().getType(); 737// // The column represents a "joined" value if it has a content metadata (except if it's the last one). 738// if ((type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) && metaDefIt.hasNext()) 739// { 740// joinedMetadata = true; 741// } 742// } 743// return joinedMetadata; 744// } 745 746 } 747 748}