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.net.URLDecoder; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.collections.CollectionUtils; 032import org.apache.commons.lang3.StringUtils; 033import org.apache.solr.client.solrj.SolrClient; 034import org.apache.solr.client.solrj.SolrQuery; 035import org.apache.solr.client.solrj.SolrQuery.ORDER; 036import org.apache.solr.client.solrj.SolrRequest.METHOD; 037import org.apache.solr.client.solrj.response.FacetField; 038import org.apache.solr.client.solrj.response.FacetField.Count; 039import org.apache.solr.client.solrj.response.QueryResponse; 040import org.slf4j.Logger; 041 042import org.ametys.cms.search.SearchField; 043import org.ametys.cms.search.SearchResults; 044import org.ametys.cms.search.Sort; 045import org.ametys.cms.search.Sort.Order; 046import org.ametys.cms.search.filter.AccessSearchFilter; 047import org.ametys.cms.search.filter.AccessSearchFilterExtensionPoint; 048import org.ametys.cms.search.query.Query; 049import org.ametys.cms.search.query.QuerySyntaxException; 050import org.ametys.core.user.CurrentUserProvider; 051import org.ametys.plugins.repository.AmetysObject; 052import org.ametys.plugins.repository.AmetysObjectIterable; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.runtime.plugin.component.AbstractLogEnabled; 055 056/** 057 * Component searching objects corresponding to a {@link Query}. 058 */ 059public class SearcherFactory extends AbstractLogEnabled implements Component, Serviceable, Initializable 060{ 061 062 /** The component role. */ 063 public static final String ROLE = SearcherFactory.class.getName(); 064 065 /** The {@link AmetysObjectResolver} */ 066 protected AmetysObjectResolver _resolver; 067 068 /** The solr client provider */ 069 protected SolrClientProvider _solrClientProvider; 070 071 /** The current user provider. */ 072 protected CurrentUserProvider _currentUserProvider; 073 074 /** The search filter extension point. */ 075 protected AccessSearchFilterExtensionPoint _searchFilterEP; 076 077 /** The solr client */ 078 protected SolrClient _solrClient; 079 080 @Override 081 public void service(ServiceManager serviceManager) throws ServiceException 082 { 083 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 084 _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE); 085 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 086 _searchFilterEP = (AccessSearchFilterExtensionPoint) serviceManager.lookup(AccessSearchFilterExtensionPoint.ROLE); 087 } 088 089 @Override 090 public void initialize() throws Exception 091 { 092 _solrClient = _solrClientProvider.getReadClient(); 093 } 094 095 /** 096 * Create a Searcher. 097 * @return a Searcher object. 098 */ 099 public Searcher create() 100 { 101 return new Searcher(getLogger()); 102 } 103 104 /** 105 * Class searching objects corresponding to a query, with optional sort, facets, and so on. 106 */ 107 public class Searcher 108 { 109 private Logger _logger; 110 111 private String _queryString; 112 private Query _query; 113 private List<String> _filterQueryStrings; 114 private List<Query> _filterQueries; 115 private List<Sort> _sortClauses; 116 private List<SearchField> _facets; 117 private Map<String, List<String>> _facetValues; 118 private int _start; 119 private int _maxResults; 120 private Map<String, Object> _searchContext; 121 private boolean _checkRights; 122 123 /** 124 * Build a Searcher with default values. 125 * @param logger The logger. 126 */ 127 protected Searcher(Logger logger) 128 { 129 _logger = logger; 130 131 _filterQueryStrings = new ArrayList<>(); 132 _filterQueries = new ArrayList<>(); 133 _sortClauses = new ArrayList<>(); 134 _facets = new ArrayList<>(); 135 _facetValues = new HashMap<>(); 136 _start = 0; 137 _maxResults = Integer.MAX_VALUE; 138 _searchContext = new HashMap<>(); 139 _checkRights = true; 140 } 141 142 /** 143 * Set the query (as a String). 144 * @param query the query (as a String). 145 * @return The Searcher object itself. 146 */ 147 public Searcher withQueryString(String query) 148 { 149 if (this._query != null) 150 { 151 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 152 } 153 this._queryString = query; 154 return this; 155 } 156 157 /** 158 * Set the query (as a {@link Query} object). 159 * @param query the query (as a {@link Query} object). 160 * @return The Searcher object itself. 161 */ 162 public Searcher withQuery(Query query) 163 { 164 if (this._queryString != null) 165 { 166 throw new IllegalArgumentException("Query and query string can't be used at the same time."); 167 } 168 this._query = query; 169 return this; 170 } 171 172 /** 173 * Set the filter queries (as Strings). 174 * @param queries the filter queries (as Strings). 175 * @return The Searcher object itself. The Searcher object itself. 176 */ 177 public Searcher withFilterQueryStrings(String... queries) 178 { 179 _filterQueryStrings = new ArrayList<>(queries.length); 180 CollectionUtils.addAll(_filterQueryStrings, queries); 181 return this; 182 } 183 184 /** 185 * Set the filter queries (as Strings). 186 * @param queries the filter queries (as Strings). 187 * @return The Searcher object itself. The Searcher object itself. 188 */ 189 public Searcher withFilterQueryStrings(Collection<String> queries) 190 { 191 _filterQueryStrings = new ArrayList<>(queries); 192 return this; 193 } 194 195 /** 196 * Add a filter query to the existing ones (as a String). 197 * @param query the filter query to add (as a String). 198 * @return The Searcher object itself. The Searcher object itself. 199 */ 200 public Searcher addFilterQueryString(String query) 201 { 202 _filterQueryStrings.add(query); 203 return this; 204 } 205 206 /** 207 * Set the filter queries (as {@link Query} objects). 208 * @param queries the filter queries (as {@link Query} objects). 209 * @return The Searcher object itself. The Searcher object itself. 210 */ 211 public Searcher withFilterQueries(Query... queries) 212 { 213 _filterQueries = new ArrayList<>(queries.length); 214 CollectionUtils.addAll(_filterQueries, queries); 215 return this; 216 } 217 218 /** 219 * Set the filter queries (as {@link Query} objects). 220 * @param queries the filter queries (as {@link Query} objects). 221 * @return The Searcher object itself. The Searcher object itself. 222 */ 223 public Searcher withFilterQueries(Collection<Query> queries) 224 { 225 _filterQueries = new ArrayList<>(queries); 226 return this; 227 } 228 229 /** 230 * Add a filter query to the existing ones (as a {@link Query} object). 231 * @param query the filter query to add (as a {@link Query} object). 232 * @return The Searcher object itself. The Searcher object itself. 233 */ 234 public Searcher addFilterQuery(Query query) 235 { 236 _filterQueries.add(query); 237 return this; 238 } 239 240 /** 241 * Set the sort clauses. 242 * @param sortClauses the sort clauses. 243 * @return The Searcher object itself. 244 */ 245 public Searcher withSort(Sort... sortClauses) 246 { 247 _sortClauses = new ArrayList<>(sortClauses.length); 248 CollectionUtils.addAll(_sortClauses, sortClauses); 249 return this; 250 } 251 252 /** 253 * Set the sort clauses. 254 * @param sortClauses the sort clauses. 255 * @return The Searcher object itself. 256 */ 257 public Searcher withSort(List<Sort> sortClauses) 258 { 259 _sortClauses = new ArrayList<>(sortClauses); 260 return this; 261 } 262 263 /** 264 * Add a sort clause to the existing ones. 265 * @param sortClause The sort clause to add. 266 * @return The Searcher object itself. 267 */ 268 public Searcher addSort(Sort sortClause) 269 { 270 _sortClauses.add(sortClause); 271 return this; 272 } 273 274 /** 275 * Set the faceted fields. 276 * @param facets the faceted fields. 277 * @return The Searcher object itself. 278 */ 279 public Searcher withFacets(SearchField... facets) 280 { 281 _facets = new ArrayList<>(facets.length); 282 CollectionUtils.addAll(_facets, facets); 283 return this; 284 } 285 286 /** 287 * Set the faceted fields. 288 * @param facets the faceted fields. 289 * @return The Searcher object itself. 290 */ 291 public Searcher withFacets(Collection<SearchField> facets) 292 { 293 _facets = new ArrayList<>(facets); 294 return this; 295 } 296 297 /** 298 * Add a faceted field. 299 * @param facet The faceted field to add. 300 * @return The Searcher object itself. 301 */ 302 public Searcher addFacet(SearchField facet) 303 { 304 _facets.add(facet); 305 return this; 306 } 307 308 /** 309 * Set the facet values. 310 * @param facetValues The facet values. 311 * @return The Searcher object itself. 312 */ 313 public Searcher withFacetValues(Map<String, List<String>> facetValues) 314 { 315 _facetValues = new HashMap<>(facetValues); 316 return this; 317 } 318 319 /** 320 * Set the search offset and limit. 321 * @param start The start index (offset). 322 * @param maxResults The maximum number of results. 323 * @return The Searcher object itself. 324 */ 325 public Searcher withLimits(int start, int maxResults) 326 { 327 this._start = start; 328 this._maxResults = maxResults; 329 return this; 330 } 331 332 /** 333 * Set the search context. 334 * @param searchContext The search context. 335 * @return The Searcher object itself. 336 */ 337 public Searcher withContext(Map<String, Object> searchContext) 338 { 339 _searchContext = new HashMap<>(searchContext); 340 return this; 341 } 342 343 /** 344 * Add a value to the search context. 345 * @param key The context key. 346 * @param value The value. 347 * @return The Searcher object itself. 348 */ 349 public Searcher addContextElement(String key, Object value) 350 { 351 _searchContext.put(key, value); 352 return this; 353 } 354 355 /** 356 * Whether to check rights when searching, false otherwise. 357 * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise. 358 * @return The Searcher object itself. 359 */ 360 public Searcher setCheckRights(boolean checkRights) 361 { 362 _checkRights = checkRights; 363 return this; 364 } 365 366 /** 367 * Execute the search with the current parameters. 368 * @param <A> The type of search results 369 * @return An iterable on the result ametys objects. 370 * @throws Exception If an error occurs. 371 */ 372 public <A extends AmetysObject> AmetysObjectIterable<A> search() throws Exception 373 { 374 SearchResults<A> searchResults = searchWithFacets(); 375 return searchResults.getObjects(); 376 } 377 378 /** 379 * Execute the search with the current parameters. 380 * @param <A> The type of search results 381 * @return An iterable on the search result objects. 382 * @throws Exception If an error occurs. 383 */ 384 public <A extends AmetysObject> SearchResults<A> searchWithFacets() throws Exception 385 { 386 QueryResponse response = _querySolrServer(); 387 Map<String, Map<String, Integer>> facetResults = getFacetResults(response, _facets); 388 389 return new SolrSearchResults<>(response, _resolver, facetResults); 390 } 391 392 private QueryResponse _querySolrServer() throws Exception 393 { 394 String query = getQuery(); 395 List<String> filterQueries = getFilterQueries(); 396 397 SolrQuery solrQuery = getSolrQuery(query, filterQueries, _start, _maxResults, _searchContext, _checkRights); 398 399 // Set the sort specification and facets in the solr query object. 400 setSort(solrQuery, _sortClauses); 401 setFacets(solrQuery, _facets, _facetValues); 402 403 if (_logger.isInfoEnabled()) 404 { 405 _logger.info("Solr query: " + URLDecoder.decode(solrQuery.toString(), "UTF-8")); 406 } 407 408 // Use POST to avoid problems with large requests. 409 return _solrClient.query(_solrClientProvider.getCollectionName(), solrQuery, METHOD.POST); 410 } 411 412 /** 413 * Get the query string from the parameters. 414 * @return The query string. 415 * @throws QuerySyntaxException If the query is invalid. 416 */ 417 protected String getQuery() throws QuerySyntaxException 418 { 419 String query = "*:*"; 420 if (_queryString != null) 421 { 422 query = _queryString; 423 } 424 else if (_query != null) 425 { 426 query = _query.build(); 427 } 428 return query; 429 } 430 431 /** 432 * Get the filter queries from the parameters. 433 * @return The list of filter queries. 434 * @throws QuerySyntaxException If one of the queries is invalid. 435 */ 436 protected List<String> getFilterQueries() throws QuerySyntaxException 437 { 438 List<String> filterQueries = new ArrayList<>(); 439 if (!_filterQueryStrings.isEmpty()) 440 { 441 filterQueries = _filterQueryStrings; 442 } 443 444 if (!_filterQueries.isEmpty()) 445 { 446 for (Query fq : _filterQueries) 447 { 448 filterQueries.add(fq.build()); 449 } 450 } 451 452 return filterQueries; 453 } 454 455 /** 456 * Get the solr query object. 457 * @param query The solr query string. 458 * @param filterQueries The filter queries (as Strings). 459 * @param start The start index. 460 * @param maxResults The maximum number of results. 461 * @param searchContext The search context. 462 * @param checkRights Whether to check rights when searching or not. 463 * @return The solr query object. 464 * @throws Exception If an error occurs. 465 */ 466 protected SolrQuery getSolrQuery(String query, Collection<String> filterQueries, int start, int maxResults, Map<String, Object> searchContext, boolean checkRights) throws Exception 467 { 468 SolrQuery solrQuery = new SolrQuery(); 469 470 String queryString = StringUtils.isNotBlank(query) ? query : "*:*"; 471 472 // Set the query string, pagination spec and fields to be returned. 473 solrQuery.setQuery(queryString); 474 solrQuery.setStart(start); 475 solrQuery.setRows(maxResults); 476 // Return only ID + score fields. 477 solrQuery.setFields("id", "score"); 478 479 // Add filter queries. 480 for (String fq : filterQueries) 481 { 482 solrQuery.addFilterQuery(fq); 483 } 484 485 if (checkRights) 486 { 487 for (String filterId : _searchFilterEP.getExtensionsIds()) 488 { 489 AccessSearchFilter filter = _searchFilterEP.getExtension(filterId); 490 for (Query fQuery : filter.getFilterQueries(searchContext)) 491 { 492 String fq = fQuery.build(); 493 if (StringUtils.isNotBlank(fq)) 494 { 495 solrQuery.addFilterQuery(fq); 496 } 497 } 498 } 499 } 500 501 return solrQuery; 502 } 503 504 /** 505 * Set the sort definition in the solr query object. 506 * @param solrQuery The solr query object. 507 * @param sortCriteria The sort criteria. 508 */ 509 protected void setSort(SolrQuery solrQuery, List<Sort> sortCriteria) 510 { 511 for (Sort sortCriterion : sortCriteria) 512 { 513 solrQuery.addSort(sortCriterion.getField(), sortCriterion.getOrder() == Order.ASC ? ORDER.asc : ORDER.desc); 514 } 515 } 516 517 /** 518 * Set the facet definition in the solr query object and return a mapping from solr field name to criterion ID. 519 * @param solrQuery the solr query object to fill. 520 * @param facets The facet definitions to use. 521 * @param facetValues the facet values. 522 */ 523 protected void setFacets(SolrQuery solrQuery, Collection<SearchField> facets, Map<String, List<String>> facetValues) 524 { 525 if (facets.size() > 0) 526 { 527 // unlimited facet values 528 solrQuery.setFacetLimit(-1); 529 } 530 531 for (SearchField facetField : facets) 532 { 533 String fieldName = facetField.getName(); 534 String solrFieldName = facetField.getFacetField(); 535 536 if (StringUtils.isNotBlank(fieldName)) 537 { 538 String facetFieldDef = "{!ex=" + fieldName + " key=" + fieldName + "}" + solrFieldName; 539 540 List<String> fieldFacetValues = facetValues.get(fieldName); 541 if (fieldFacetValues != null && !fieldFacetValues.isEmpty()) 542 { 543 StringBuilder fq = new StringBuilder(); 544 fq.append("{!tag=").append(fieldName).append("}("); 545 546 int i = 0; 547 for (String facetValue : fieldFacetValues) 548 { 549 if (i > 0) 550 { 551 fq.append(" OR "); 552 } 553 // TODO escape query chars? 554 fq.append(solrFieldName).append(":\"").append(facetValue).append('"'); 555 i++; 556 } 557 fq.append(')'); 558 solrQuery.addFilterQuery(fq.toString()); 559 } 560 561 solrQuery.addFacetField(facetFieldDef); 562 } 563 } 564 } 565 566 /** 567 * Retrieve the facet results from the solr response. 568 * @param response the solr response. 569 * @param facets The facet fields to return. 570 * @return the facet results. 571 */ 572 protected Map<String, Map<String, Integer>> getFacetResults(QueryResponse response, Collection<SearchField> facets) 573 { 574 Map<String, Map<String, Integer>> facetResults = new LinkedHashMap<>(); 575 576 for (SearchField facetField : facets) 577 { 578 String fieldName = facetField.getName(); 579 FacetField solrFacetField = response.getFacetField(fieldName); 580 581 List<Count> values = solrFacetField.getValues(); 582 583 Map<String, Integer> solrFacetValues = new HashMap<>(); 584 facetResults.put(fieldName, solrFacetValues); 585 586 for (Count count : values) 587 { 588 solrFacetValues.put(count.getName(), (int) count.getCount()); 589 } 590 } 591 592 return facetResults; 593 } 594 } 595 596}