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.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.OutputStream; 021import java.nio.charset.StandardCharsets; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.Set; 030import java.util.stream.Collectors; 031 032import org.apache.avalon.framework.activity.Initializable; 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.collections.CollectionUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.solr.client.solrj.SolrClient; 040import org.apache.solr.client.solrj.request.RequestWriter.ContentWriter; 041import org.apache.solr.client.solrj.request.json.DomainMap; 042import org.apache.solr.client.solrj.request.json.JsonQueryRequest; 043import org.apache.solr.client.solrj.request.json.TermsFacetMap; 044import org.apache.solr.client.solrj.response.FacetField; 045import org.apache.solr.client.solrj.response.FacetField.Count; 046import org.apache.solr.client.solrj.response.QueryResponse; 047import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet; 048import org.apache.solr.client.solrj.response.json.BucketJsonFacet; 049import org.apache.solr.common.params.CommonParams; 050import org.slf4j.Logger; 051 052import org.ametys.cms.search.SearchResults; 053import org.ametys.cms.search.SortOrder; 054import org.ametys.cms.search.query.JoinQuery; 055import org.ametys.cms.search.query.MatchAllQuery; 056import org.ametys.cms.search.query.OrQuery; 057import org.ametys.cms.search.query.Query; 058import org.ametys.cms.search.query.QuerySyntaxException; 059import org.ametys.core.group.GroupIdentity; 060import org.ametys.core.group.GroupManager; 061import org.ametys.core.right.AllowedUsers; 062import org.ametys.core.user.CurrentUserProvider; 063import org.ametys.core.user.UserIdentity; 064import org.ametys.plugins.repository.AmetysObject; 065import org.ametys.plugins.repository.AmetysObjectIterable; 066import org.ametys.plugins.repository.AmetysObjectResolver; 067import org.ametys.runtime.plugin.component.AbstractLogEnabled; 068 069/** 070 * Component searching objects corresponding to a {@link Query}. 071 */ 072public class SearcherFactory extends AbstractLogEnabled implements Component, Serviceable, Initializable 073{ 074 075 /** The component role. */ 076 public static final String ROLE = SearcherFactory.class.getName(); 077 078 /** The {@link AmetysObjectResolver} */ 079 protected AmetysObjectResolver _resolver; 080 081 /** The solr client provider */ 082 protected SolrClientProvider _solrClientProvider; 083 084 /** The current user provider. */ 085 protected CurrentUserProvider _currentUserProvider; 086 087 /** The group manager */ 088 protected GroupManager _groupManager; 089 090 /** The solr client */ 091 protected SolrClient _solrClient; 092 093 @Override 094 public void service(ServiceManager serviceManager) throws ServiceException 095 { 096 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 097 _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE); 098 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 099 _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE); 100 } 101 102 @Override 103 public void initialize() throws Exception 104 { 105 _solrClient = _solrClientProvider.getReadClient(); 106 } 107 108 /** 109 * Create a Searcher. 110 * @return a Searcher object. 111 */ 112 public Searcher create() 113 { 114 return new Searcher(getLogger()); 115 } 116 117 /** 118 * Clone a Searcher. 119 * @param searcher The Searcher to clone. 120 * @return A new Searcher object with the same parameters as the given one. 121 */ 122 public Searcher cloneSearcher(Searcher searcher) 123 { 124 Searcher clonedSearcher = new Searcher(getLogger()); 125 clonedSearcher._queryString = searcher._queryString; 126 clonedSearcher._query = searcher._query; 127 clonedSearcher._filterQueryStrings = new ArrayList<>(searcher._filterQueryStrings); 128 clonedSearcher._filterQueries = new ArrayList<>(searcher._filterQueries); 129 clonedSearcher._sortClauses = new ArrayList<>(searcher._sortClauses); 130 clonedSearcher._facets = new ArrayList<>(searcher._facets); 131 clonedSearcher._facetValues = new HashMap<>(searcher._facetValues); 132 clonedSearcher._start = searcher._start; 133 clonedSearcher._maxResults = searcher._maxResults; 134 clonedSearcher._searchContext = new HashMap<>(searcher._searchContext); 135 clonedSearcher._checkRights = searcher._checkRights; 136 clonedSearcher._checkRightsComparingTo = searcher._checkRightsComparingTo; 137 clonedSearcher._debug = searcher._debug; 138 return clonedSearcher; 139 } 140 141 /** 142 * Class searching objects corresponding to a query, with optional sort, facets, and so on. 143 */ 144 public class Searcher 145 { 146 private Logger _logger; 147 148 private String _queryString; 149 private Query _query; 150 private List<String> _filterQueryStrings; 151 private List<Query> _filterQueries; 152 private List<SortDefinition> _sortClauses; 153 private List<FacetDefinition> _facets; 154 private Map<String, List<String>> _facetValues; 155 private int _start; 156 private int _maxResults; 157 private Map<String, Object> _searchContext; 158 private boolean _checkRights; 159 private AllowedUsers _checkRightsComparingTo; 160 private boolean _debug; 161 162 /** 163 * Build a Searcher with default values. 164 * @param logger The logger. 165 */ 166 protected Searcher(Logger logger) 167 { 168 _logger = logger; 169 170 _filterQueryStrings = new ArrayList<>(); 171 _filterQueries = new ArrayList<>(); 172 _sortClauses = new ArrayList<>(); 173 _facets = new ArrayList<>(); 174 _facetValues = new HashMap<>(); 175 _start = 0; 176 _maxResults = Integer.MAX_VALUE; 177 _searchContext = new HashMap<>(); 178 _checkRights = true; 179 } 180 181 /** 182 * Set the query (as a String). 183 * @param query the query (as a String). 184 * @return The Searcher object itself. 185 */ 186 public Searcher withQueryString(String query) 187 { 188 if (this._query != null) 189 { 190 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 191 } 192 this._queryString = query; 193 return this; 194 } 195 196 /** 197 * Set the query (as a {@link Query} object). 198 * @param query the query (as a {@link Query} object). 199 * @return The Searcher object itself. 200 */ 201 public Searcher withQuery(Query query) 202 { 203 if (this._queryString != null) 204 { 205 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 206 } 207 this._query = query; 208 return this; 209 } 210 211 /** 212 * Set the filter queries (as Strings). 213 * @param queries the filter queries (as Strings). 214 * @return The Searcher object itself. The Searcher object itself. 215 */ 216 public Searcher withFilterQueryStrings(String... queries) 217 { 218 _filterQueryStrings = new ArrayList<>(queries.length); 219 CollectionUtils.addAll(_filterQueryStrings, queries); 220 return this; 221 } 222 223 /** 224 * Set the filter queries (as Strings). 225 * @param queries the filter queries (as Strings). 226 * @return The Searcher object itself. The Searcher object itself. 227 */ 228 public Searcher withFilterQueryStrings(Collection<String> queries) 229 { 230 _filterQueryStrings = new ArrayList<>(queries); 231 return this; 232 } 233 234 /** 235 * Add a filter query to the existing ones (as a String). 236 * @param query the filter query to add (as a String). 237 * @return The Searcher object itself. The Searcher object itself. 238 */ 239 public Searcher addFilterQueryString(String query) 240 { 241 _filterQueryStrings.add(query); 242 return this; 243 } 244 245 /** 246 * Set the filter queries (as {@link Query} objects). 247 * @param queries the filter queries (as {@link Query} objects). 248 * @return The Searcher object itself. The Searcher object itself. 249 */ 250 public Searcher withFilterQueries(Query... queries) 251 { 252 _filterQueries = new ArrayList<>(queries.length); 253 CollectionUtils.addAll(_filterQueries, queries); 254 return this; 255 } 256 257 /** 258 * Set the filter queries (as {@link Query} objects). 259 * @param queries the filter queries (as {@link Query} objects). 260 * @return The Searcher object itself. The Searcher object itself. 261 */ 262 public Searcher withFilterQueries(Collection<Query> queries) 263 { 264 _filterQueries = new ArrayList<>(queries); 265 return this; 266 } 267 268 /** 269 * Add a filter query to the existing ones (as a {@link Query} object). 270 * @param query the filter query to add (as a {@link Query} object). 271 * @return The Searcher object itself. The Searcher object itself. 272 */ 273 public Searcher addFilterQuery(Query query) 274 { 275 _filterQueries.add(query); 276 return this; 277 } 278 279 /** 280 * Set the sort clauses. 281 * @param sortClauses the sort clauses. 282 * @return The Searcher object itself. 283 */ 284 public Searcher withSort(SortDefinition... sortClauses) 285 { 286 _sortClauses = new ArrayList<>(sortClauses.length); 287 CollectionUtils.addAll(_sortClauses, sortClauses); 288 return this; 289 } 290 291 /** 292 * Set the sort clauses. 293 * @param sortClauses the sort clauses. 294 * @return The Searcher object itself. 295 */ 296 public Searcher withSort(List<SortDefinition> sortClauses) 297 { 298 _sortClauses = new ArrayList<>(sortClauses); 299 return this; 300 } 301 302 /** 303 * Add a sort clause to the existing ones. 304 * @param sortClause The sort clause to add. 305 * @return The Searcher object itself. 306 */ 307 public Searcher addSort(SortDefinition sortClause) 308 { 309 _sortClauses.add(sortClause); 310 return this; 311 } 312 313 /** 314 * Set the faceted fields. 315 * @param facets the faceted fields. 316 * @return The Searcher object itself. 317 */ 318 public Searcher withFacets(FacetDefinition... facets) 319 { 320 _facets = new ArrayList<>(facets.length); 321 CollectionUtils.addAll(_facets, facets); 322 return this; 323 } 324 325 /** 326 * Set the faceted fields. 327 * @param facets the faceted fields. 328 * @return The Searcher object itself. 329 */ 330 public Searcher withFacets(Collection<FacetDefinition> facets) 331 { 332 _facets = new ArrayList<>(facets); 333 return this; 334 } 335 336 /** 337 * Add a faceted field. 338 * @param facet The faceted field to add. 339 * @return The Searcher object itself. 340 */ 341 public Searcher addFacet(FacetDefinition facet) 342 { 343 _facets.add(facet); 344 return this; 345 } 346 347 /** 348 * Set the facet values. 349 * @param facetValues The facet values. 350 * @return The Searcher object itself. 351 */ 352 public Searcher withFacetValues(Map<String, List<String>> facetValues) 353 { 354 _facetValues = new HashMap<>(facetValues); 355 return this; 356 } 357 358 /** 359 * Set the search offset and limit. 360 * @param start The start index (offset). 361 * @param maxResults The maximum number of results. 362 * @return The Searcher object itself. 363 */ 364 public Searcher withLimits(int start, int maxResults) 365 { 366 this._start = start; 367 this._maxResults = maxResults; 368 return this; 369 } 370 371 /** 372 * Set the search context. 373 * @param searchContext The search context. 374 * @return The Searcher object itself. 375 */ 376 public Searcher withContext(Map<String, Object> searchContext) 377 { 378 _searchContext = new HashMap<>(searchContext); 379 return this; 380 } 381 382 /** 383 * Add a value to the search context. 384 * @param key The context key. 385 * @param value The value. 386 * @return The Searcher object itself. 387 */ 388 public Searcher addContextElement(String key, Object value) 389 { 390 _searchContext.put(key, value); 391 return this; 392 } 393 394 /** 395 * Whether to check rights when searching, false otherwise. 396 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 397 * @return The Searcher object itself. 398 */ 399 public Searcher setCheckRights(boolean checkRights) 400 { 401 _checkRights = checkRights; 402 return this; 403 } 404 405 /** 406 * Check rights when searching, <b>not</b> according to the current user, 407 * but according to the given {@link AllowedUsers visibilty} to compare each 408 * result with. 409 * @param compareTo the {@link AllowedUsers visibilty} to compare each result with. 410 * @return The Searcher object itself. 411 */ 412 public Searcher checkRightsComparingTo(AllowedUsers compareTo) 413 { 414 _checkRights = false; 415 _checkRightsComparingTo = compareTo; 416 return this; 417 } 418 419 /** 420 * Sets the debug on the Solr query 421 * @return The Searcher object itself. 422 */ 423 public Searcher setDebugOn() 424 { 425 _debug = true; 426 return this; 427 } 428 429 /** 430 * Execute the search with the current parameters. 431 * @param <A> The type of search results 432 * @return An iterable on the result ametys objects. 433 * @throws Exception If an error occurs. 434 */ 435 public <A extends AmetysObject> AmetysObjectIterable<A> search() throws Exception 436 { 437 SearchResults<A> searchResults = searchWithFacets(); 438 return searchResults.getObjects(); 439 } 440 441 /** 442 * Execute the search with the current parameters. 443 * @param <A> The type of search results 444 * @return An iterable on the search result objects. 445 * @throws Exception If an error occurs. 446 */ 447 public <A extends AmetysObject> SearchResults<A> searchWithFacets() throws Exception 448 { 449 QueryResponse response = _querySolrServer(); 450 return _buildResults(response, _facets); 451 } 452 453 /** 454 * From the Solr server response, builds the {@link SearchResults} object. 455 * @param <A> The type of search results 456 * @param response The response from the Solr server 457 * @param facets The facet fields to return 458 * @return An iterable on the search result objects. 459 * @throws Exception If an error occurs. 460 */ 461 protected <A extends AmetysObject> SearchResults<A> _buildResults(QueryResponse response, List<FacetDefinition> facets) throws Exception 462 { 463 _handleDebug(response); 464 Map<String, Map<String, Integer>> facetResults = getFacetResults(response, facets); 465 return new SolrSearchResults<>(response, _resolver, facetResults); 466 } 467 468 private void _handleDebug(QueryResponse response) 469 { 470 if (_debug && _logger.isDebugEnabled()) 471 { 472 Map<String, Object> debugMap = response.getDebugMap(); 473 _logger.debug("Debug response: \n{}", debugMap); 474 } 475 } 476 477 private QueryResponse _querySolrServer() throws Exception 478 { 479 _logSearchQueries(); 480 481 Object query = getQuery(); 482 List<Object> filterQueries = getFilterQueries(); 483 484 AmetysQueryRequest solrQuery = getSolrQuery(query, filterQueries, _start, _maxResults, _searchContext, _checkRights, _checkRightsComparingTo); 485 486 // Set the sort specification and facets in the solr query object. 487 setSort(solrQuery, _sortClauses); 488 setFacets(solrQuery, _facets, _facetValues); 489 490 modifySolrQuery(solrQuery); 491 492 QueryResponse response = solrQuery.process(_solrClient, _solrClientProvider.getCollectionName()); 493 494 if (_logger.isInfoEnabled()) 495 { 496 _logger.info("Solr request executed in {} ms", response.getQTime()); 497 } 498 499 return response; 500 } 501 502 private void _logSearchQueries() 503 { 504 if (!_logger.isDebugEnabled()) 505 { 506 return; 507 } 508 509 if (_queryString == null && _query != null) 510 { 511 _logger.debug("Query before building: \n{}", _query.toString(0)); 512 } 513 514 if (!_filterQueries.isEmpty()) 515 { 516 _logger.debug("Filter Queries before building: \n{}", _filterQueries 517 .stream() 518 .map(fq -> fq.toString(0)) 519 .collect(Collectors.joining("\n###\n"))); 520 } 521 } 522 523 /** 524 * Get the query string from the parameters. 525 * @return The query string. 526 * @throws QuerySyntaxException If the query is invalid. 527 */ 528 protected Object getQuery() throws QuerySyntaxException 529 { 530 Object query = "*:*"; 531 532 if (_queryString != null) 533 { 534 query = _queryString; 535 } 536 else if (_query != null) 537 { 538 query = _query.rewrite().orElse(new MatchAllQuery()) 539 .buildAsJson().orElse(new MatchAllQuery().buildAsJson()); 540 } 541 542 return query; 543 } 544 545 /** 546 * Get the filter queries from the parameters. 547 * @return The list of filter queries. 548 * @throws QuerySyntaxException If one of the queries is invalid. 549 */ 550 protected List<Object> getFilterQueries() throws QuerySyntaxException 551 { 552 List<Object> filterQueries = new ArrayList<>(); 553 554 filterQueries.addAll(_filterQueryStrings); 555 556 for (Query fq : _filterQueries) 557 { 558 // discard useless empty or "*:*" filter queries 559 Optional<Query> query = fq.rewrite(); 560 if (query.isPresent() && !(query.get() instanceof MatchAllQuery)) 561 { 562 Optional<Object> fqAsJson = query.get().buildAsJson(); 563 if (fqAsJson.isPresent()) 564 { 565 filterQueries.add(fqAsJson.get()); 566 } 567 } 568 } 569 570 return filterQueries; 571 } 572 573 /** 574 * Get the solr query object. 575 * @param query The solr query string. 576 * @param filterQueries The filter queries (as Strings). 577 * @param start The start index. 578 * @param maxResults The maximum number of results. 579 * @param searchContext The search context. 580 * @param checkRights Whether to check rights when searching or not. 581 * @param allowedUsersToCompare The {@link AllowedUsers} object to compare with for checking rights 582 * @return The solr query object. 583 * @throws Exception If an error occurs. 584 */ 585 @SuppressWarnings("unchecked") 586 protected AmetysQueryRequest getSolrQuery(Object query, Collection<Object> filterQueries, int start, int maxResults, Map<String, Object> searchContext, boolean checkRights, AllowedUsers allowedUsersToCompare) throws Exception 587 { 588 AmetysQueryRequest solrQuery = new AmetysQueryRequest(_logger); 589 590 if (query instanceof String q) 591 { 592 solrQuery.setQuery(StringUtils.isNotBlank(q) ? q : "*:*"); 593 } 594 else if (query instanceof Map) 595 { 596 solrQuery.setQuery((Map<String, Object>) query); 597 } 598 599 // Set the query string, pagination spec and fields to be returned. 600 if (start > 0) 601 { 602 solrQuery.setOffset(start); 603 } 604 605 solrQuery.setLimit(maxResults); 606 607 // Return only ID + score fields. 608 solrQuery.returnFields("id", "score"); 609 610 // Add filter queries. 611 for (Object fq : filterQueries) 612 { 613 if (fq instanceof String) 614 { 615 solrQuery.withFilter((String) fq); 616 } 617 else if (fq instanceof Map) 618 { 619 solrQuery.withFilter((Map<String, Object>) fq); 620 } 621 } 622 623 if (checkRights) 624 { 625 _checkRightsQuery(solrQuery); 626 } 627 else if (allowedUsersToCompare != null) 628 { 629 _checkAllowedUsers(solrQuery, allowedUsersToCompare); 630 } 631 632 if (_debug) 633 { 634 solrQuery.withParam(CommonParams.DEBUG, "true"); 635 } 636 637 return solrQuery; 638 } 639 640 private void _checkRightsQuery(JsonQueryRequest solrQuery) 641 { 642 Map<String, Object> acl; 643 644 UserIdentity user = _currentUserProvider.getUser(); 645 if (user == null) 646 { 647 acl = Map.of("anonymous", ""); 648 } 649 else 650 { 651 acl = new HashMap<>(); 652 acl.put("populationId", user.getPopulationId()); 653 acl.put("login", user.getLogin()); 654 655 Set<GroupIdentity> groups = _groupManager.getUserGroups(user); 656 if (!groups.isEmpty()) 657 { 658 String groupsAsStr = groups.stream() 659 .map(GroupIdentity::groupIdentityToString) 660 .collect(Collectors.joining(",")); 661 acl.put("groups", groupsAsStr); 662 } 663 } 664 665 // {!acl anonymous=} or {!acl populationId=users login=user1 groups="group1#groupDirectory,group2#groupDirectory"} to check acl 666 solrQuery.withFilter(Map.of("acl", acl)); 667 } 668 669 private void _checkAllowedUsers(JsonQueryRequest solrQuery, AllowedUsers allowedUsersToCompare) 670 { 671 Map<String, Object> acl; 672 673 if (allowedUsersToCompare.isAnonymousAllowed()) 674 { 675 acl = Map.of("anonymous", "true"); 676 } 677 else 678 { 679 acl = new HashMap<>(); 680 681 if (allowedUsersToCompare.isAnyConnectedUserAllowed()) 682 { 683 acl.put("anyConnected", "true"); 684 } 685 686 Set<String> allowedUsers = allowedUsersToCompare.getAllowedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet()); 687 Set<String> deniedUsers = allowedUsersToCompare.getDeniedUsers().stream().map(UserIdentity::userIdentityToString).collect(Collectors.toSet()); 688 Set<String> allowedGroups = allowedUsersToCompare.getAllowedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet()); 689 Set<String> deniedGroups = allowedUsersToCompare.getDeniedGroups().stream().map(GroupIdentity::groupIdentityToString).collect(Collectors.toSet()); 690 691 if (!allowedUsers.isEmpty()) 692 { 693 acl.put("allowedUsers", String.join(",", allowedUsers)); 694 } 695 696 if (!deniedUsers.isEmpty()) 697 { 698 acl.put("deniedUsers", String.join(",", deniedUsers)); 699 } 700 701 if (!allowedGroups.isEmpty()) 702 { 703 acl.put("allowedGroups", String.join(",", allowedGroups)); 704 } 705 706 if (!deniedGroups.isEmpty()) 707 { 708 acl.put("deniedGroups", String.join(",", deniedGroups)); 709 } 710 } 711 712 solrQuery.withFilter(Map.of("aclCompare", acl)); 713 } 714 715 /** 716 * Set the sort definition in the solr query object. 717 * @param solrQuery The solr query object. 718 * @param sortCriteria The sort criteria. 719 */ 720 protected void setSort(JsonQueryRequest solrQuery, List<SortDefinition> sortCriteria) 721 { 722 if (sortCriteria.isEmpty()) 723 { 724 solrQuery.setSort("score desc"); 725 } 726 727 String sort = sortCriteria.stream() 728 .map(sortCriterion -> _getSortFinalFieldName(sortCriterion) + " " + (sortCriterion.order() == SortOrder.ASC ? "asc" : "desc")) 729 .collect(Collectors.joining(",")); 730 731 if (StringUtils.isNotBlank(sort)) 732 { 733 solrQuery.setSort(sort); 734 } 735 } 736 737 private String _getSortFinalFieldName(SortDefinition sortCriterion) 738 { 739 return sortCriterion.joinedPaths().isEmpty() 740 ? sortCriterion.solrSortFieldName() 741 : "ametys(" + _getJoinedFunction(sortCriterion.joinedPaths(), sortCriterion.solrSortFieldName()) + ")"; 742 } 743 744 /** 745 * Set the facet definition in the solr query object and return a mapping from solr field name to criterion ID. 746 * @param solrQuery the solr query object to fill. 747 * @param facetDefinitions The facet definitions to use. 748 * @param facetValues the facet values. 749 * @throws QuerySyntaxException if there's a syntax error in queries 750 */ 751 protected void setFacets(JsonQueryRequest solrQuery, Collection<FacetDefinition> facetDefinitions, Map<String, List<String>> facetValues) throws QuerySyntaxException 752 { 753 List<String> joinedFacets = new ArrayList<>(); 754 for (FacetDefinition facetDefinition : facetDefinitions) 755 { 756 String fieldName = facetDefinition.id(); 757 String facetFieldName = facetDefinition.solrFacetFieldName(); 758 759 if (StringUtils.isNotBlank(fieldName)) 760 { 761 List<String> joinedPaths = facetDefinition.joinedPaths(); 762 if (joinedPaths.isEmpty()) 763 { 764 _setNonJoinedFacet(solrQuery, fieldName, facetFieldName, facetValues); 765 } 766 else 767 { 768 String joinedFacet = _setJoinedFacet(solrQuery, fieldName, joinedPaths, facetFieldName, facetValues); 769 joinedFacets.add(joinedFacet); 770 } 771 } 772 } 773 774 if (!joinedFacets.isEmpty()) 775 { 776 solrQuery.withParam("facet", "true"); 777 solrQuery.withParam("facet.ametys", joinedFacets); 778 } 779 } 780 781 private String _setJoinedFacet(JsonQueryRequest solrQuery, String fieldName, List<String> joinedPaths, String solrFieldName, Map<String, List<String>> facetValues) throws QuerySyntaxException 782 { 783 List<String> fieldFacetValues = facetValues.get(fieldName); 784 if (fieldFacetValues != null && !fieldFacetValues.isEmpty()) 785 { 786 List<Query> facetQueries = new ArrayList<>(); 787 for (String facetValue : fieldFacetValues) 788 { 789 facetQueries.add(() -> solrFieldName + ":\"" + facetValue + '"'); 790 } 791 792 JoinQuery joinQuery = new JoinQuery(new OrQuery(facetQueries), joinedPaths); 793 794 solrQuery.withFilter(Map.of("#" + fieldName, joinQuery.buildAsJson().orElse(new MatchAllQuery().buildAsJson()))); 795 } 796 797 return "{!ex=" + fieldName + " key=" + fieldName + "}" + _getJoinedFunction(joinedPaths, solrFieldName); 798 } 799 800 private String _getJoinedFunction(List<String> joinedPaths, String solrFieldName) 801 { 802 return StringUtils.join(joinedPaths, "->") + "," + solrFieldName; 803 } 804 805 private void _setNonJoinedFacet(JsonQueryRequest solrQuery, String fieldName, String solrFieldName, Map<String, List<String>> facetValues) throws QuerySyntaxException 806 { 807 List<String> fieldFacetValues = facetValues.get(fieldName); 808 if (fieldFacetValues != null && !fieldFacetValues.isEmpty()) 809 { 810 List<Query> facetQueries = new ArrayList<>(); 811 for (String facetValue : fieldFacetValues) 812 { 813 facetQueries.add(() -> solrFieldName + ":\"" + facetValue + '"'); 814 } 815 816 solrQuery.withFilter(Map.of("#" + fieldName, new OrQuery(facetQueries).buildAsJson().orElse(new MatchAllQuery().buildAsJson()))); 817 } 818 819 TermsFacetMap facetMap = new TermsFacetMap(solrFieldName).setLimit(-1) 820 .withDomain(new DomainMap().withTagsToExclude(fieldName)); 821 822 solrQuery.withFacet(fieldName, facetMap); 823 } 824 825 /** 826 * Retrieve the facet results from the solr response. 827 * @param response the solr response. 828 * @param facetDefinitions The facet fields to return. 829 * @return the facet results. 830 */ 831 protected Map<String, Map<String, Integer>> getFacetResults(QueryResponse response, Collection<FacetDefinition> facetDefinitions) 832 { 833 Map<String, Map<String, Integer>> facetResults = new LinkedHashMap<>(); 834 835 for (FacetDefinition facetDefinition : facetDefinitions) 836 { 837 String fieldName = facetDefinition.id(); 838 839 if (facetDefinition.joinedPaths().isEmpty()) 840 { 841 BucketBasedJsonFacet facet = response.getJsonFacetingResponse().getBucketBasedFacets(fieldName); 842 843 List<BucketJsonFacet> values = facet.getBuckets(); 844 845 Map<String, Integer> solrFacetValues = new HashMap<>(); 846 facetResults.put(fieldName, solrFacetValues); 847 848 for (BucketJsonFacet value : values) 849 { 850 solrFacetValues.put(value.getVal().toString(), (int) value.getCount()); 851 } 852 } 853 else 854 { 855 FacetField solrFacetField = response.getFacetField(fieldName); 856 857 List<Count> values = solrFacetField.getValues(); 858 859 Map<String, Integer> solrFacetValues = new HashMap<>(); 860 facetResults.put(fieldName, solrFacetValues); 861 862 for (Count count : values) 863 { 864 solrFacetValues.put(count.getName(), (int) count.getCount()); 865 } 866 } 867 } 868 869 return facetResults; 870 } 871 872 /** 873 * Template method to do additional operations on the Solr query before passing it to the Solr client 874 * @param query the Solr query 875 */ 876 protected void modifySolrQuery(JsonQueryRequest query) 877 { 878 // do nothing by default 879 } 880 } 881 882 static class AmetysQueryRequest extends JsonQueryRequest 883 { 884 private Logger _logger; 885 886 public AmetysQueryRequest(Logger logger) 887 { 888 super(); 889 _logger = logger; 890 } 891 892 @Override 893 public ContentWriter getContentWriter(String expectedType) 894 { 895 ContentWriter writer = super.getContentWriter(expectedType); 896 897 if (!_logger.isInfoEnabled()) 898 { 899 return writer; 900 } 901 902 return new ContentWriter() 903 { 904 public void write(OutputStream os) throws IOException 905 { 906 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 907 writer.write(baos); 908 909 _logger.info("Solr query:\n" + baos.toString(StandardCharsets.UTF_8)); 910 911 os.write(baos.toByteArray(), 0, baos.size()); 912 } 913 914 public String getContentType() 915 { 916 return writer.getContentType(); 917 } 918 }; 919 } 920 } 921 922 /** 923 * Record representing a sort criterion. 924 * @param solrSortFieldName the name of the solr sort field 925 * @param joinedPaths the joined paths 926 * @param order The sort order 927 */ 928 public record SortDefinition (String solrSortFieldName, List<String> joinedPaths, SortOrder order) { 929 930 /** 931 * Creates a {@link SortDefinition} record 932 * @param solrSortFieldName the name of the solr sort field 933 * @param order The sort order 934 */ 935 public SortDefinition(String solrSortFieldName, SortOrder order) 936 { 937 this(solrSortFieldName, new ArrayList<>(), order); 938 } 939 } 940 941 /** 942 * Record representing a facet definition. 943 * @param id the facet identifier 944 * @param solrFacetFieldName the name of the solr facet field 945 * @param joinedPaths the joined paths 946 */ 947 public record FacetDefinition (String id, String solrFacetFieldName, List<String> joinedPaths) { 948 949 /** 950 * Creates a {@link FacetDefinition} record 951 * @param id the facet identifier 952 * @param solrFacetFieldName the name of the solr facet field 953 */ 954 public FacetDefinition(String id, String solrFacetFieldName) 955 { 956 this(id, solrFacetFieldName, new ArrayList<>()); 957 } 958 } 959}