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