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