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