001/* 002 * File : $Source$ 003 * Date : $Date$ 004 * Version: $Revision$ 005 * 006 * This library is part of OpenCms - 007 * the Open Source Content Management System 008 * 009 * Copyright (C) 2002 - 2009 Alkacon Software (https://www.alkacon.com) 010 * 011 * This library is free software; you can redistribute it and/or 012 * modify it under the terms of the GNU Lesser General Public 013 * License as published by the Free Software Foundation; either 014 * version 2.1 of the License, or (at your option) any later version. 015 * 016 * This library is distributed in the hope that it will be useful, 017 * but WITHOUT ANY WARRANTY; without even the implied warranty of 018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 * Lesser General Public License for more details. 020 * 021 * For further information about Alkacon Software, please see the 022 * company website: https://www.alkacon.com 023 * 024 * For further information about OpenCms, please see the 025 * project website: https://www.opencms.org 026 * 027 * You should have received a copy of the GNU Lesser General Public 028 * License along with this library; if not, write to the Free Software 029 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 030 */ 031 032package org.opencms.search.solr; 033 034import org.opencms.configuration.CmsConfigurationException; 035import org.opencms.configuration.CmsParameterConfiguration; 036import org.opencms.file.CmsFile; 037import org.opencms.file.CmsObject; 038import org.opencms.file.CmsProject; 039import org.opencms.file.CmsProperty; 040import org.opencms.file.CmsPropertyDefinition; 041import org.opencms.file.CmsResource; 042import org.opencms.file.CmsResourceFilter; 043import org.opencms.i18n.CmsEncoder; 044import org.opencms.i18n.CmsLocaleManager; 045import org.opencms.main.CmsException; 046import org.opencms.main.CmsIllegalArgumentException; 047import org.opencms.main.CmsLog; 048import org.opencms.main.OpenCms; 049import org.opencms.report.I_CmsReport; 050import org.opencms.search.CmsSearchException; 051import org.opencms.search.CmsSearchIndex; 052import org.opencms.search.CmsSearchManager; 053import org.opencms.search.CmsSearchParameters; 054import org.opencms.search.CmsSearchResource; 055import org.opencms.search.CmsSearchResultList; 056import org.opencms.search.I_CmsIndexWriter; 057import org.opencms.search.I_CmsSearchDocument; 058import org.opencms.search.extractors.I_CmsExtractionResult; 059import org.opencms.search.fields.CmsSearchField; 060import org.opencms.search.galleries.CmsGallerySearchParameters; 061import org.opencms.search.galleries.CmsGallerySearchResult; 062import org.opencms.search.galleries.CmsGallerySearchResultList; 063import org.opencms.security.CmsRole; 064import org.opencms.security.CmsRoleViolationException; 065import org.opencms.util.CmsFileUtil; 066import org.opencms.util.CmsRequestUtil; 067import org.opencms.util.CmsStringUtil; 068 069import java.io.IOException; 070import java.io.UnsupportedEncodingException; 071import java.io.Writer; 072import java.nio.charset.Charset; 073import java.util.ArrayList; 074import java.util.Arrays; 075import java.util.Collections; 076import java.util.HashSet; 077import java.util.List; 078import java.util.Locale; 079import java.util.Set; 080import java.util.stream.Collectors; 081import java.util.stream.Stream; 082 083import javax.servlet.ServletResponse; 084 085import org.apache.commons.logging.Log; 086import org.apache.solr.client.solrj.SolrClient; 087import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer; 088import org.apache.solr.client.solrj.request.SolrQuery; 089import org.apache.solr.client.solrj.response.QueryResponse; 090import org.apache.solr.common.SolrDocument; 091import org.apache.solr.common.SolrDocumentList; 092import org.apache.solr.common.SolrInputDocument; 093import org.apache.solr.common.util.NamedList; 094import org.apache.solr.common.util.SimpleOrderedMap; 095import org.apache.solr.core.CoreContainer; 096import org.apache.solr.core.SolrCore; 097import org.apache.solr.handler.ReplicationHandler; 098import org.apache.solr.request.LocalSolrQueryRequest; 099import org.apache.solr.request.SolrQueryRequest; 100import org.apache.solr.request.SolrRequestHandler; 101import org.apache.solr.response.QueryResponseWriter; 102import org.apache.solr.response.SolrQueryResponse; 103 104import com.google.common.base.Objects; 105 106/** 107 * Implements the search within an Solr index.<p> 108 * 109 * @since 8.5.0 110 */ 111public class CmsSolrIndex extends CmsSearchIndex { 112 113 /** The serial version id. */ 114 private static final long serialVersionUID = -1570077792574476721L; 115 116 /** The name of the default Solr Offline index. */ 117 public static final String DEFAULT_INDEX_NAME_OFFLINE = "Solr Offline"; 118 119 /** The name of the default Solr Online index. */ 120 public static final String DEFAULT_INDEX_NAME_ONLINE = "Solr Online"; 121 122 /** Constant for additional parameter to set the post processor class name. */ 123 public static final String POST_PROCESSOR = "search.solr.postProcessor"; 124 125 /** Constant for additional parameter to set an index-specific document transformer. */ 126 public static final String DOCUMENT_TRANSFORMER = "search.solr.documentTransformer"; 127 128 /** 129 * Constant for additional parameter to set the maximally processed results (start + rows) for searches with this index. 130 * It overwrites the global configuration from {@link CmsSolrConfiguration#getMaxProcessedResults()} for this index. 131 **/ 132 public static final String SOLR_SEARCH_MAX_PROCESSED_RESULTS = "search.solr.maxProcessedResults"; 133 134 /** Constant for additional parameter to set the fields the select handler should return at maximum. */ 135 public static final String SOLR_HANDLER_ALLOWED_FIELDS = "handle.solr.allowedFields"; 136 137 /** Constant for additional parameter to set the number results the select handler should return at maxium per request. */ 138 public static final String SOLR_HANDLER_MAX_ALLOWED_RESULTS_PER_PAGE = "handle.solr.maxAllowedResultsPerPage"; 139 140 /** Constant for additional parameter to set the maximal number of a result, the select handler should return. */ 141 public static final String SOLR_HANDLER_MAX_ALLOWED_RESULTS_AT_ALL = "handle.solr.maxAllowedResultsAtAll"; 142 143 /** Constant for additional parameter to disable the select handler (except for debug mode). */ 144 private static final String SOLR_HANDLER_DISABLE_SELECT = "handle.solr.disableSelectHandler"; 145 146 /** Constant for additional parameter to set the VFS path to the file holding the debug secret. */ 147 private static final String SOLR_HANDLER_DEBUG_SECRET_FILE = "handle.solr.debugSecretFile"; 148 149 /** Constant for additional parameter to disable the spell handler (except for debug mode). */ 150 private static final String SOLR_HANDLER_DISABLE_SPELL = "handle.solr.disableSpellHandler"; 151 152 /** Constant for additional parameter to configure an external solr server specifically for the index. */ 153 private static final String SOLR_SERVER_URL = "server.url"; 154 155 /** The solr exclude property. */ 156 public static final String PROPERTY_SEARCH_EXCLUDE_VALUE_SOLR = "solr"; 157 158 /** Indicates the maximum number of documents from the complete result set to return. */ 159 public static final int ROWS_MAX = 50; 160 161 /** The constant for an unlimited maximum number of results to return in a Solr search. */ 162 public static final int MAX_RESULTS_UNLIMITED = -1; 163 164 /** The constant for an unlimited maximum number of results to return in a Solr search. */ 165 public static final int MAX_RESULTS_GALLERY = 10000; 166 167 /** A constant for debug formatting output. */ 168 protected static final int DEBUG_PADDING_RIGHT = 50; 169 170 /** The name for the parameters key of the response header. */ 171 private static final String HEADER_PARAMS_NAME = "params"; 172 173 /** The log object for this class. */ 174 private static final Log LOG = CmsLog.getLog(CmsSolrIndex.class); 175 176 /** Pseudo resource used for not permission checked indexes. */ 177 private static final CmsResource PSEUDO_RES = new CmsResource( 178 null, 179 null, 180 null, 181 0, 182 false, 183 0, 184 null, 185 null, 186 0L, 187 null, 188 0L, 189 null, 190 0L, 191 0L, 192 0, 193 0, 194 0L, 195 0); 196 197 /** The name of the key that is used for the result documents inside the Solr query response. */ 198 private static final String QUERY_RESPONSE_NAME = "response"; 199 200 /** The name of the key that is used for the query time. */ 201 private static final String QUERY_TIME_NAME = "QTime"; 202 203 /** The name of the key that is used for the query time. */ 204 private static final String QUERY_HIGHLIGHTING_NAME = "highlighting"; 205 206 /** A constant for UTF-8 charset. */ 207 private static final Charset UTF8 = Charset.forName("UTF-8"); 208 209 /** The name of the request parameter holding the debug secret. */ 210 private static final String REQUEST_PARAM_DEBUG_SECRET = "_debug"; 211 212 /** The name of the query parameter enabling spell checking. */ 213 private static final String QUERY_SPELLCHECK_NAME = "spellcheck"; 214 215 /** The name of the query parameter sorting. */ 216 private static final String QUERY_SORT_NAME = "sort"; 217 218 /** The name of the query parameter expand. */ 219 private static final String QUERY_PARAM_EXPAND = "expand"; 220 221 /** The embedded Solr client for this index. */ 222 transient SolrClient m_solr; 223 224 /** The post document manipulator. */ 225 private transient I_CmsSolrPostSearchProcessor m_postProcessor; 226 227 /** The index specific document transformer. */ 228 private transient I_CmsSolrDocumentTransformer m_documentTransformer; 229 230 /** The core name for the index. */ 231 private transient String m_coreName; 232 233 /** The list of allowed fields to return. */ 234 private String[] m_handlerAllowedFields; 235 236 /** The number of maximally allowed results per page when using the handler. */ 237 private int m_handlerMaxAllowedResultsPerPage = -1; 238 239 /** The number of maximally allowed results at all when using the handler. */ 240 private int m_handlerMaxAllowedResultsAtAll = -1; 241 242 /** Flag, indicating if the handler only works in debug mode. */ 243 private boolean m_handlerSelectDisabled; 244 245 /** Path to the secret file. Must be under /system/.../ or /shared/.../ and readable by all users that should be able to debug. */ 246 private String m_handlerDebugSecretFile; 247 248 /** Flag, indicating if the spellcheck handler is disabled for the index. */ 249 private boolean m_handlerSpellDisabled; 250 251 /** The maximal number of results to process for search queries. */ 252 int m_maxProcessedResults = -2; // special value for not initialized. 253 254 /** Server URL to use specific for the index. If set, it overwrites all other server settings. */ 255 private String m_serverUrl; 256 257 /** 258 * Default constructor.<p> 259 */ 260 public CmsSolrIndex() { 261 262 super(); 263 } 264 265 /** 266 * Public constructor to create a Solr index.<p> 267 * 268 * @param name the name for this index.<p> 269 * 270 * @throws CmsIllegalArgumentException if something goes wrong 271 */ 272 public CmsSolrIndex(String name) 273 throws CmsIllegalArgumentException { 274 275 super(name); 276 } 277 278 /** 279 * Returns the resource type for the given root path.<p> 280 * 281 * @param cms the current CMS context 282 * @param rootPath the root path of the resource to get the type for 283 * 284 * @return the resource type for the given root path 285 */ 286 public static final String getType(CmsObject cms, String rootPath) { 287 288 String type = null; 289 CmsSolrIndex index = CmsSearchManager.getIndexSolr(cms, null); 290 if (index != null) { 291 I_CmsSearchDocument doc = index.getDocument(CmsSearchField.FIELD_PATH, rootPath); 292 if (doc != null) { 293 type = doc.getFieldValueAsString(CmsSearchField.FIELD_TYPE); 294 } 295 } 296 return type; 297 } 298 299 /** 300 * @see org.opencms.search.CmsSearchIndex#addConfigurationParameter(java.lang.String, java.lang.String) 301 */ 302 @Override 303 public void addConfigurationParameter(String key, String value) { 304 305 switch (key) { 306 case POST_PROCESSOR: 307 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 308 try { 309 setPostProcessor((I_CmsSolrPostSearchProcessor)Class.forName(value).newInstance()); 310 } catch (Exception e) { 311 CmsException ex = new CmsException( 312 Messages.get().container(Messages.LOG_SOLR_ERR_POST_PROCESSOR_NOT_EXIST_1, value), 313 e); 314 LOG.error(ex.getMessage(), ex); 315 } 316 } 317 break; 318 case DOCUMENT_TRANSFORMER: 319 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 320 try { 321 Class<I_CmsSolrDocumentTransformer> transformerClass = (Class<I_CmsSolrDocumentTransformer>)Class.forName( 322 value); 323 setDocumentTransformer(transformerClass.getDeclaredConstructor().newInstance()); 324 } catch (Exception e) { 325 CmsException ex = new CmsException( 326 Messages.get().container(Messages.LOG_SOLR_ERR_DOCUMENT_TRANSFORMER_NOT_EXIST_1, value), 327 e); 328 LOG.error(ex.getMessage(), ex); 329 } 330 } 331 break; 332 case SOLR_HANDLER_ALLOWED_FIELDS: 333 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 334 m_handlerAllowedFields = Stream.of(value.split(",")).map(v -> v.trim()).toArray(String[]::new); 335 } 336 break; 337 case SOLR_HANDLER_MAX_ALLOWED_RESULTS_PER_PAGE: 338 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 339 try { 340 m_handlerMaxAllowedResultsPerPage = Integer.parseInt(value); 341 } catch (NumberFormatException e) { 342 LOG.warn( 343 "Could not parse parameter \"" 344 + SOLR_HANDLER_MAX_ALLOWED_RESULTS_PER_PAGE 345 + "\" for index \"" 346 + getName() 347 + "\". Results per page will not be restricted."); 348 } 349 } 350 break; 351 case SOLR_HANDLER_MAX_ALLOWED_RESULTS_AT_ALL: 352 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 353 try { 354 m_handlerMaxAllowedResultsAtAll = Integer.parseInt(value); 355 } catch (NumberFormatException e) { 356 LOG.warn( 357 "Could not parse parameter \"" 358 + SOLR_HANDLER_MAX_ALLOWED_RESULTS_AT_ALL 359 + "\" for index \"" 360 + getName() 361 + "\". Results per page will not be restricted."); 362 } 363 } 364 break; 365 case SOLR_HANDLER_DISABLE_SELECT: 366 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 367 m_handlerSelectDisabled = value.trim().toLowerCase().equals("true"); 368 } 369 break; 370 case SOLR_HANDLER_DEBUG_SECRET_FILE: 371 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 372 m_handlerDebugSecretFile = value.trim(); 373 } 374 break; 375 case SOLR_HANDLER_DISABLE_SPELL: 376 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 377 m_handlerSpellDisabled = value.trim().toLowerCase().equals("true"); 378 } 379 break; 380 case SOLR_SEARCH_MAX_PROCESSED_RESULTS: 381 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 382 try { 383 m_maxProcessedResults = Integer.parseInt(value); 384 } catch (NumberFormatException e) { 385 LOG.warn( 386 "Could not parse parameter \"" 387 + SOLR_SEARCH_MAX_PROCESSED_RESULTS 388 + "\" for index \"" 389 + getName() 390 + "\". The global configuration will be used instead."); 391 } 392 } 393 break; 394 case SOLR_SERVER_URL: 395 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 396 m_serverUrl = value.trim(); 397 } 398 break; 399 default: 400 super.addConfigurationParameter(key, value); 401 break; 402 } 403 } 404 405 /** 406 * @see org.opencms.search.I_CmsSearchIndex#applyDocumentTransformation(org.opencms.search.I_CmsSearchDocument, org.opencms.file.CmsObject, org.opencms.file.CmsResource, org.opencms.search.extractors.I_CmsExtractionResult, java.util.List, java.util.List) 407 */ 408 @Override 409 public I_CmsSearchDocument applyDocumentTransformation( 410 I_CmsSearchDocument doc, 411 CmsObject cms, 412 CmsResource resource, 413 I_CmsExtractionResult extractionResult, 414 List<CmsProperty> properties, 415 List<CmsProperty> propertiesSearched) { 416 417 if (null != m_documentTransformer) { 418 return m_documentTransformer.transform( 419 doc, 420 cms, 421 resource, 422 extractionResult, 423 properties, 424 propertiesSearched); 425 } 426 return doc; 427 } 428 429 /** 430 * @see org.opencms.search.CmsSearchIndex#createEmptyDocument(org.opencms.file.CmsResource) 431 */ 432 @Override 433 public I_CmsSearchDocument createEmptyDocument(CmsResource resource) { 434 435 CmsSolrDocument doc = new CmsSolrDocument(new SolrInputDocument()); 436 doc.setId(resource.getStructureId()); 437 return doc; 438 } 439 440 /** 441 * @see org.opencms.search.CmsSearchIndex#createIndexWriter(boolean, org.opencms.report.I_CmsReport) 442 */ 443 @Override 444 public I_CmsIndexWriter createIndexWriter(boolean create, I_CmsReport report) { 445 446 return new CmsSolrIndexWriter(m_solr, this); 447 } 448 449 /** 450 * @see org.opencms.search.CmsSearchIndex#excludeFromIndex(CmsObject, CmsResource) 451 */ 452 @Override 453 public boolean excludeFromIndex(CmsObject cms, CmsResource resource) { 454 455 if (resource.isFolder() || resource.isTemporaryFile()) { 456 // don't index folders or temporary files for galleries, but pretty much everything else 457 return true; 458 } 459 // If this is the default offline index than it is used for gallery search that needs all resources indexed. 460 if (this.getName().equals(DEFAULT_INDEX_NAME_OFFLINE)) { 461 return false; 462 } 463 464 boolean isOnlineIndex = getProject().equals(CmsProject.ONLINE_PROJECT_NAME); 465 if (isOnlineIndex && (resource.getDateExpired() <= System.currentTimeMillis())) { 466 return true; 467 } 468 469 try { 470 // do property lookup with folder search 471 String propValue = cms.readPropertyObject( 472 resource, 473 CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE, 474 true).getValue(); 475 if (propValue != null) { 476 if (!(propValue.trim().toLowerCase().startsWith("false"))) { 477 return true; 478 } 479 } 480 } catch (CmsException e) { 481 if (LOG.isDebugEnabled()) { 482 LOG.debug( 483 org.opencms.search.Messages.get().getBundle().key( 484 org.opencms.search.Messages.LOG_UNABLE_TO_READ_PROPERTY_1, 485 resource.getRootPath())); 486 } 487 } 488 if (!USE_ALL_LOCALE.equalsIgnoreCase(getLocale().getLanguage())) { 489 // check if any resource default locale has a match with the index locale, if not skip resource 490 List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource); 491 Locale match = OpenCms.getLocaleManager().getFirstMatchingLocale( 492 Collections.singletonList(getLocale()), 493 locales); 494 return (match == null); 495 } 496 return false; 497 498 } 499 500 /** 501 * Performs a search with according to the gallery search parameters.<p> 502 * 503 * @param cms the cms context 504 * @param params the search parameters 505 * 506 * @return the search result 507 */ 508 public CmsGallerySearchResultList gallerySearch(CmsObject cms, CmsGallerySearchParameters params) { 509 510 CmsGallerySearchResultList resultList = new CmsGallerySearchResultList(); 511 if (params.isForceEmptyResult()) { 512 return resultList; 513 } 514 515 try { 516 CmsSolrResultList list = search( 517 cms, 518 params.getQuery(cms), 519 false, 520 null, 521 true, 522 CmsResourceFilter.ONLY_VISIBLE_NO_DELETED, 523 MAX_RESULTS_GALLERY); // ignore the maximally searched number of contents. 524 525 if (null == list) { 526 return null; 527 } 528 529 resultList.setHitCount(Long.valueOf(list.getNumFound()).intValue()); 530 for (CmsSearchResource resource : list) { 531 I_CmsSearchDocument document = resource.getDocument(); 532 Locale locale = CmsLocaleManager.getLocale(params.getLocale()); 533 534 CmsGallerySearchResult result = new CmsGallerySearchResult( 535 document, 536 cms, 537 (int)document.getScore(), 538 locale); 539 540 resultList.add(result); 541 } 542 } catch (CmsSearchException e) { 543 LOG.error(e.getMessage(), e); 544 } 545 return resultList; 546 } 547 548 /** 549 * @see org.opencms.search.CmsSearchIndex#getConfiguration() 550 */ 551 @Override 552 public CmsParameterConfiguration getConfiguration() { 553 554 CmsParameterConfiguration result = super.getConfiguration(); 555 if (getPostProcessor() != null) { 556 result.put(POST_PROCESSOR, getPostProcessor().getClass().getName()); 557 } 558 return result; 559 } 560 561 /** 562 * Returns the name of the core of the index. 563 * NOTE: Index and core name differ since OpenCms 10.5 due to new naming rules for cores in SOLR. 564 * 565 * @return the name of the core of the index. 566 */ 567 public String getCoreName() { 568 569 return m_coreName; 570 } 571 572 /** 573 * @see org.opencms.search.CmsSearchIndex#getDocument(java.lang.String, java.lang.String) 574 */ 575 @Override 576 public synchronized I_CmsSearchDocument getDocument(String fieldname, String term) { 577 578 return getDocument(fieldname, term, null); 579 } 580 581 /** 582 * Version of {@link org.opencms.search.CmsSearchIndex#getDocument(java.lang.String, java.lang.String)} where 583 * the returned fields can be restricted. 584 * 585 * @param fieldname the field to query in 586 * @param term the query 587 * @param fls the returned fields. 588 * @return the document. 589 */ 590 public synchronized I_CmsSearchDocument getDocument(String fieldname, String term, String[] fls) { 591 592 try { 593 SolrQuery query = new SolrQuery(); 594 if (CmsSearchField.FIELD_PATH.equals(fieldname)) { 595 query.setQuery(fieldname + ":\"" + term + "\""); 596 } else { 597 query.setQuery(fieldname + ":" + term); 598 } 599 // We could have more than one document due to serial dates. We only want one arbitrary document per id/path 600 query.setRows(Integer.valueOf(1)); 601 if (null != fls) { 602 query.setFields(fls); 603 } 604 QueryResponse res = m_solr.query(getCoreName(), query); 605 if (res != null) { 606 SolrDocumentList sdl = m_solr.query(getCoreName(), query).getResults(); 607 if ((sdl.getNumFound() > 0L) && (sdl.get(0) != null)) { 608 return new CmsSolrDocument(sdl.get(0)); 609 } 610 } 611 } catch (Exception e) { 612 // ignore and assume that the document could not be found 613 LOG.error(e.getMessage(), e); 614 } 615 return null; 616 } 617 618 /** 619 * Returns the language locale for the given resource in this index.<p> 620 * 621 * @param cms the current OpenCms user context 622 * @param resource the resource to check 623 * @param availableLocales a list of locales supported by the resource 624 * 625 * @return the language locale for the given resource in this index 626 */ 627 @Override 628 public Locale getLocaleForResource(CmsObject cms, CmsResource resource, List<Locale> availableLocales) { 629 630 Locale result = null; 631 List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource); 632 if ((availableLocales != null) && (availableLocales.size() > 0)) { 633 result = OpenCms.getLocaleManager().getBestMatchingLocale( 634 defaultLocales.get(0), 635 defaultLocales, 636 availableLocales); 637 } 638 if (result == null) { 639 result = ((availableLocales != null) && availableLocales.isEmpty()) 640 ? availableLocales.get(0) 641 : defaultLocales.get(0); 642 } 643 return result; 644 } 645 646 /** 647 * Returns the maximal number of results (start + rows) that are processed for each search query unless another 648 * maximum is explicitly specified in {@link #search(CmsObject, CmsSolrQuery, boolean, ServletResponse, boolean, CmsResourceFilter, int)}. 649 * 650 * @return the maximal number of results (start + rows) that are processed for a search query. 651 */ 652 public int getMaxProcessedResults() { 653 654 return m_maxProcessedResults; 655 } 656 657 /** 658 * Returns the search post processor.<p> 659 * 660 * @return the post processor to use 661 */ 662 public I_CmsSolrPostSearchProcessor getPostProcessor() { 663 664 return m_postProcessor; 665 } 666 667 /** 668 * Returns the Solr server URL to connect to for this specific index, or <code>null</code> if no specific URL is configured. 669 * @return the Solr server URL to connect to for this specific index, or <code>null</code> if no specific URL is configured. 670 */ 671 public String getServerUrl() { 672 673 return m_serverUrl; 674 } 675 676 /** 677 * @see org.opencms.search.CmsSearchIndex#initialize() 678 */ 679 @Override 680 public void initialize() throws CmsSearchException { 681 682 super.initialize(); 683 if (m_maxProcessedResults == -2) { 684 m_maxProcessedResults = OpenCms.getSearchManager().getSolrServerConfiguration().getMaxProcessedResults(); 685 } 686 try { 687 OpenCms.getSearchManager().registerSolrIndex(this); 688 } catch (CmsConfigurationException ex) { 689 LOG.error(ex.getMessage(), ex); 690 setEnabled(false); 691 } 692 } 693 694 /** Returns a flag, indicating if the Solr server is not yet set. 695 * @return a flag, indicating if the Solr server is not yet set. 696 */ 697 public boolean isNoSolrServerSet() { 698 699 return null == m_solr; 700 } 701 702 /** 703 * Not yet implemented for Solr.<p> 704 * 705 * <code> 706 * #################<br> 707 * ### DON'T USE ###<br> 708 * #################<br> 709 * </code> 710 * 711 * @deprecated Use {@link #search(CmsObject, SolrQuery)} or {@link #search(CmsObject, String)} instead 712 */ 713 @Override 714 @Deprecated 715 public synchronized CmsSearchResultList search(CmsObject cms, CmsSearchParameters params) { 716 717 throw new UnsupportedOperationException(); 718 } 719 720 /** 721 * Default search method.<p> 722 * 723 * @param cms the current CMS object 724 * @param query the query 725 * 726 * @return the results 727 * 728 * @throws CmsSearchException if something goes wrong 729 * 730 * @see #search(CmsObject, String) 731 */ 732 public CmsSolrResultList search(CmsObject cms, CmsSolrQuery query) throws CmsSearchException { 733 734 return search(cms, query, false); 735 } 736 737 /** 738 * Performs a search.<p> 739 * 740 * Returns a list of 'OpenCms resource documents' 741 * ({@link CmsSearchResource}) encapsulated within the class {@link CmsSolrResultList}. 742 * This list can be accessed exactly like an {@link List} which entries are 743 * {@link CmsSearchResource} that extend {@link CmsResource} and holds the Solr 744 * implementation of {@link I_CmsSearchDocument} as member. <b>This enables you to deal 745 * with the resulting list as you do with well known {@link List} and work on it's entries 746 * like you do on {@link CmsResource}.</b> 747 * 748 * <h4>What will be done with the Solr search result?</h4> 749 * <ul> 750 * <li>Although it can happen, that there are less results returned than rows were requested 751 * (imagine an index containing less documents than requested rows) we try to guarantee 752 * the requested amount of search results and to provide a working pagination with 753 * security check.</li> 754 * 755 * <li>To be sure we get enough documents left even the permission check reduces the amount 756 * of found documents, the rows are multiplied by <code>'5'</code> and the current page 757 * additionally the offset is added. The count of documents we don't have enough 758 * permissions for grows with increasing page number, that's why we also multiply 759 * the rows by the current page count.</li> 760 * 761 * <li>Also make sure we perform the permission check for all found documents, so start with 762 * the first found doc.</li> 763 * </ul> 764 * 765 * <b>NOTE:</b> If latter pages than the current one are containing protected documents the 766 * total hit count will be incorrect, because the permission check ends if we have 767 * enough results found for the page to display. With other words latter pages than 768 * the current can contain documents that will first be checked if those pages are 769 * requested to be displayed, what causes a incorrect hit count.<p> 770 * 771 * @param cms the current OpenCms context 772 * @param ignoreMaxRows <code>true</code> to return all all requested rows, <code>false</code> to use max rows 773 * @param query the OpenCms Solr query 774 * 775 * @return the list of found documents 776 * 777 * @throws CmsSearchException if something goes wrong 778 * 779 * @see org.opencms.search.solr.CmsSolrResultList 780 * @see org.opencms.search.CmsSearchResource 781 * @see org.opencms.search.I_CmsSearchDocument 782 * @see org.opencms.search.solr.CmsSolrQuery 783 */ 784 public CmsSolrResultList search(CmsObject cms, final CmsSolrQuery query, boolean ignoreMaxRows) 785 throws CmsSearchException { 786 787 return search(cms, query, ignoreMaxRows, null, false, null); 788 } 789 790 /** 791 * Like {@link #search(CmsObject, CmsSolrQuery, boolean)}, but additionally a resource filter can be specified. 792 * By default, the filter depends on the index. 793 * 794 * @param cms the current OpenCms context 795 * @param ignoreMaxRows <code>true</code> to return all all requested rows, <code>false</code> to use max rows 796 * @param query the OpenCms Solr query 797 * @param filter the resource filter to use for post-processing. 798 * 799 * @return the list of documents found. 800 * 801 * @throws CmsSearchException if something goes wrong 802 */ 803 public CmsSolrResultList search( 804 CmsObject cms, 805 final CmsSolrQuery query, 806 boolean ignoreMaxRows, 807 final CmsResourceFilter filter) 808 throws CmsSearchException { 809 810 return search(cms, query, ignoreMaxRows, null, false, filter); 811 } 812 813 /** 814 * Performs the actual search.<p> 815 * 816 * @param cms the current OpenCms context 817 * @param query the OpenCms Solr query 818 * @param ignoreMaxRows <code>true</code> to return all all requested rows, <code>false</code> to use max rows 819 * @param response the servlet response to write the query result to, may also be <code>null</code> 820 * @param ignoreSearchExclude if set to false, only contents with search_exclude unset or "false" will be found - typical for the the non-gallery case 821 * @param filter the resource filter to use 822 * 823 * @return the found documents 824 * 825 * @throws CmsSearchException if something goes wrong 826 * 827 * @see #search(CmsObject, CmsSolrQuery, boolean) 828 */ 829 public CmsSolrResultList search( 830 CmsObject cms, 831 final CmsSolrQuery query, 832 boolean ignoreMaxRows, 833 ServletResponse response, 834 boolean ignoreSearchExclude, 835 CmsResourceFilter filter) 836 throws CmsSearchException { 837 838 return search(cms, query, ignoreMaxRows, response, ignoreSearchExclude, filter, getMaxProcessedResults()); 839 } 840 841 /** 842 * Performs the actual search.<p> 843 * 844 * To provide for correct permissions two queries are performed and the response is fused from that queries: 845 * <ol> 846 * <li>a query for permission checking, where fl, start and rows is adjusted. From this query result we take for the response: 847 * <ul> 848 * <li>facets</li> 849 * <li>spellcheck</li> 850 * <li>suggester</li> 851 * <li>morelikethis</li> 852 * <li>clusters</li> 853 * </ul> 854 * </li> 855 * <li>a query that collects only the resources determined by the first query and performs highlighting. From this query we take for the response: 856 * <li>result</li> 857 * <li>highlighting</li> 858 * </li> 859 *</ol> 860 * 861 * Currently not or only partly supported Solr features are: 862 * <ul> 863 * <li>groups</li> 864 * <li>collapse - representatives of the collapsed group might be filtered by the permission check</li> 865 * <li>expand is disabled</li> 866 * </ul> 867 * 868 * @param cms the current OpenCms context 869 * @param query the OpenCms Solr query 870 * @param ignoreMaxRows <code>true</code> to return all requested rows, <code>false</code> to use max rows 871 * @param response the servlet response to write the query result to, may also be <code>null</code> 872 * @param ignoreSearchExclude if set to false, only contents with search_exclude unset or "false" will be found - typical for the the non-gallery case 873 * @param filter the resource filter to use 874 * @param maxNumResults the maximal number of results to search for 875 * 876 * @return the found documents 877 * 878 * @throws CmsSearchException if something goes wrong 879 * 880 * @see #search(CmsObject, CmsSolrQuery, boolean) 881 */ 882 @SuppressWarnings("unchecked") 883 public CmsSolrResultList search( 884 CmsObject cms, 885 final CmsSolrQuery query, 886 boolean ignoreMaxRows, 887 ServletResponse response, 888 boolean ignoreSearchExclude, 889 CmsResourceFilter filter, 890 int maxNumResults) 891 throws CmsSearchException { 892 893 CmsSolrResultList result = null; 894 long startTime = System.currentTimeMillis(); 895 896 // TODO: 897 // - fall back to "last found results" if none are present at the "last page"? 898 // - deal with cursorMarks? 899 // - deal with groups? 900 // - deal with result clustering? 901 // - remove max score calculation? 902 903 if (LOG.isDebugEnabled()) { 904 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SOLR_DEBUG_ORIGINAL_QUERY_2, query, getName())); 905 } 906 907 // change thread priority in order to reduce search impact on overall system performance 908 int previousPriority = Thread.currentThread().getPriority(); 909 if (getPriority() > 0) { 910 Thread.currentThread().setPriority(getPriority()); 911 } 912 913 // check if the user is allowed to access this index 914 checkOfflineAccess(cms); 915 916 if (!ignoreSearchExclude) { 917 if (LOG.isInfoEnabled()) { 918 LOG.info( 919 Messages.get().getBundle().key( 920 Messages.LOG_SOLR_INFO_ADDING_SEARCH_EXCLUDE_FILTER_FOR_QUERY_2, 921 query, 922 getName())); 923 } 924 String fqSearchExclude = CmsSearchField.FIELD_SEARCH_EXCLUDE + ":\"false\""; 925 query.removeFilterQuery(fqSearchExclude); 926 query.addFilterQuery(fqSearchExclude); 927 } 928 929 if (CmsProject.ONLINE_PROJECT_NAME.equals(getProject())) { 930 query.addFilterQuery( 931 "-" 932 + CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE_ONLINE 933 + CmsSearchField.FIELD_DYNAMIC_PROPERTIES 934 + ":\"true\""); 935 } 936 937 // get start parameter from the request 938 int start = null == query.getStart() ? 0 : query.getStart().intValue(); 939 940 // correct negative start values to 0. 941 if (start < 0) { 942 query.setStart(Integer.valueOf(0)); 943 start = 0; 944 } 945 946 // Adjust the maximal number of results to process in case it is unlimited. 947 if (maxNumResults < 0) { 948 maxNumResults = Integer.MAX_VALUE; 949 if (LOG.isInfoEnabled()) { 950 LOG.info( 951 Messages.get().getBundle().key( 952 Messages.LOG_SOLR_INFO_LIMITING_MAX_PROCESSED_RESULTS_3, 953 query, 954 getName(), 955 Integer.valueOf(maxNumResults))); 956 } 957 } 958 959 // Correct the rows parameter 960 // Set the default rows, if rows are not set in the original query. 961 int rows = null == query.getRows() ? CmsSolrQuery.DEFAULT_ROWS.intValue() : query.getRows().intValue(); 962 963 // Restrict the rows, such that the maximal number of queryable results is not exceeded. 964 if ((((rows + start) > maxNumResults) || ((rows + start) < 0))) { 965 rows = maxNumResults - start; 966 } 967 // Restrict the rows to the maximally allowed number, if they should be restricted. 968 if (!ignoreMaxRows && (rows > ROWS_MAX)) { 969 if (LOG.isInfoEnabled()) { 970 LOG.info( 971 Messages.get().getBundle().key( 972 Messages.LOG_SOLR_INFO_LIMITING_MAX_ROWS_4, 973 new Object[] {query, getName(), Integer.valueOf(rows), Integer.valueOf(ROWS_MAX)})); 974 } 975 rows = ROWS_MAX; 976 } 977 // If start is higher than maxNumResults, the rows could be negative here - correct this. 978 if (rows < 0) { 979 if (LOG.isInfoEnabled()) { 980 LOG.info( 981 Messages.get().getBundle().key( 982 Messages.LOG_SOLR_INFO_CORRECTING_ROWS_4, 983 new Object[] {query, getName(), Integer.valueOf(rows), Integer.valueOf(0)})); 984 } 985 rows = 0; 986 } 987 // Set the corrected rows for the query. 988 query.setRows(Integer.valueOf(rows)); 989 990 // remove potentially set expand parameter 991 if (null != query.getParams(QUERY_PARAM_EXPAND)) { 992 LOG.info(Messages.get().getBundle().key(Messages.LOG_SOLR_INFO_REMOVING_EXPAND_2, query, getName())); 993 query.remove("expand"); 994 } 995 996 float maxScore = 0; 997 998 LocalSolrQueryRequest solrQueryRequest = null; 999 SolrCore core = null; 1000 String[] sortParamValues = query.getParams(QUERY_SORT_NAME); 1001 boolean sortByScoreDesc = (null == sortParamValues) 1002 || (sortParamValues.length == 0) 1003 || Objects.equal(sortParamValues[0], "score desc"); 1004 1005 try { 1006 1007 // initialize the search context 1008 CmsObject searchCms = OpenCms.initCmsObject(cms); 1009 1010 //////////////////////////////////////////////////////////////////////////////////////////////////////////////// QUERY 1011 /// FOR PERMISSION CHECK, FACETS, SPELLCHECK, SUGGESTIONS 1012 1013 // Clone the query and keep the original one 1014 CmsSolrQuery checkQuery = query.clone(); 1015 // Initialize rows, offset, end and the current page. 1016 int end = start + rows; 1017 int itemsToCheck = 0 == end ? 0 : Math.max(10, end + (end / 5)); // request 20 percent more, but at least 10 results if permissions are filtered 1018 // use a set to prevent double entries if multiple check queries are performed. 1019 Set<String> resultSolrIds = new HashSet<>(rows); // rows are set before definitely. 1020 1021 // counter for the documents found and accessible 1022 int cnt = 0; 1023 long hitCount = 0; 1024 long visibleHitCount = 0; 1025 int processedResults = 0; 1026 long solrPermissionTime = 0; 1027 // disable highlighting - it's done in the next query. 1028 checkQuery.setHighlight(false); 1029 // adjust rows and start for the permission check. 1030 checkQuery.setRows(Integer.valueOf(Math.min(maxNumResults - processedResults, itemsToCheck))); 1031 checkQuery.setStart(Integer.valueOf(processedResults)); 1032 // return only the fields required for the permission check and for scoring 1033 checkQuery.setFields(CmsSearchField.FIELD_TYPE, CmsSearchField.FIELD_SOLR_ID, CmsSearchField.FIELD_PATH); 1034 List<String> originalFields = Arrays.asList(query.getFields().split(",")); 1035 if (originalFields.contains(CmsSearchField.FIELD_SCORE)) { 1036 checkQuery.addField(CmsSearchField.FIELD_SCORE); 1037 } 1038 if (LOG.isDebugEnabled()) { 1039 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SOLR_DEBUG_CHECK_QUERY_2, checkQuery, getName())); 1040 } 1041 // perform the permission check Solr query and remember the response and time Solr took. 1042 long solrCheckTime = System.currentTimeMillis(); 1043 QueryResponse checkQueryResponse = m_solr.query(getCoreName(), checkQuery); 1044 solrCheckTime = System.currentTimeMillis() - solrCheckTime; 1045 solrPermissionTime += solrCheckTime; 1046 1047 // initialize the counts 1048 hitCount = checkQueryResponse.getResults().getNumFound(); 1049 int maxToProcess = Long.valueOf(Math.min(hitCount, maxNumResults)).intValue(); 1050 visibleHitCount = hitCount; 1051 1052 // process found documents 1053 for (SolrDocument doc : checkQueryResponse.getResults()) { 1054 try { 1055 CmsSolrDocument searchDoc = new CmsSolrDocument(doc); 1056 if (needsPermissionCheck(searchDoc) && !hasPermissions(searchCms, searchDoc, filter)) { 1057 visibleHitCount--; 1058 } else { 1059 if (cnt >= start) { 1060 resultSolrIds.add(searchDoc.getFieldValueAsString(CmsSearchField.FIELD_SOLR_ID)); 1061 } 1062 if (sortByScoreDesc && (searchDoc.getScore() > maxScore)) { 1063 maxScore = searchDoc.getScore(); 1064 } 1065 if (++cnt >= end) { 1066 break; 1067 } 1068 } 1069 } catch (Exception e) { 1070 // should not happen, but if it does we want to go on with the next result nevertheless 1071 visibleHitCount--; 1072 LOG.warn(Messages.get().getBundle().key(Messages.LOG_SOLR_ERR_RESULT_ITERATION_FAILED_0), e); 1073 } 1074 } 1075 processedResults += checkQueryResponse.getResults().size(); 1076 1077 if ((resultSolrIds.size() < rows) && (processedResults < maxToProcess)) { 1078 CmsSolrQuery secondCheckQuery = checkQuery.clone(); 1079 // disable all features not necessary, since results are present from the first check query. 1080 secondCheckQuery.setFacet(false); 1081 secondCheckQuery.setMoreLikeThis(false); 1082 secondCheckQuery.set(QUERY_SPELLCHECK_NAME, false); 1083 do { 1084 // query directly more under certain conditions to reduce number of queries 1085 itemsToCheck = itemsToCheck < 3000 ? itemsToCheck * 4 : itemsToCheck; 1086 // adjust rows and start for the permission check. 1087 secondCheckQuery.setRows( 1088 Integer.valueOf( 1089 Long.valueOf(Math.min(maxToProcess - processedResults, itemsToCheck)).intValue())); 1090 secondCheckQuery.setStart(Integer.valueOf(processedResults)); 1091 1092 if (LOG.isDebugEnabled()) { 1093 LOG.debug( 1094 Messages.get().getBundle().key( 1095 Messages.LOG_SOLR_DEBUG_SECONDCHECK_QUERY_2, 1096 secondCheckQuery, 1097 getName())); 1098 } 1099 1100 long solrSecondCheckTime = System.currentTimeMillis(); 1101 QueryResponse secondCheckQueryResponse = m_solr.query(getCoreName(), secondCheckQuery); 1102 processedResults += secondCheckQueryResponse.getResults().size(); 1103 solrSecondCheckTime = System.currentTimeMillis() - solrSecondCheckTime; 1104 solrPermissionTime += solrCheckTime; 1105 1106 // process found documents 1107 for (SolrDocument doc : secondCheckQueryResponse.getResults()) { 1108 try { 1109 CmsSolrDocument searchDoc = new CmsSolrDocument(doc); 1110 String docSolrId = searchDoc.getFieldValueAsString(CmsSearchField.FIELD_SOLR_ID); 1111 if ((needsPermissionCheck(searchDoc) && !hasPermissions(searchCms, searchDoc, filter)) 1112 || resultSolrIds.contains(docSolrId)) { 1113 visibleHitCount--; 1114 } else { 1115 if (cnt >= start) { 1116 resultSolrIds.add(docSolrId); 1117 } 1118 if (sortByScoreDesc && (searchDoc.getScore() > maxScore)) { 1119 maxScore = searchDoc.getScore(); 1120 } 1121 if (++cnt >= end) { 1122 break; 1123 } 1124 } 1125 } catch (Exception e) { 1126 // should not happen, but if it does we want to go on with the next result nevertheless 1127 visibleHitCount--; 1128 LOG.warn( 1129 Messages.get().getBundle().key(Messages.LOG_SOLR_ERR_RESULT_ITERATION_FAILED_0), 1130 e); 1131 } 1132 } 1133 1134 } while ((resultSolrIds.size() < rows) && (processedResults < maxToProcess)); 1135 } 1136 1137 //////////////////////////////////////////////////////////////////////////////////////////////////////////////// QUERY 1138 /// FOR RESULTS AND HIGHLIGHTING 1139 1140 // the lists storing the found documents that will be returned 1141 List<CmsSearchResource> resourceDocumentList = new ArrayList<CmsSearchResource>(resultSolrIds.size()); 1142 SolrDocumentList solrDocumentList = new SolrDocumentList(); 1143 1144 long solrResultTime = 0; 1145 1146 // If we're using a post-processor, (re-)initialize it before using it 1147 if (m_postProcessor != null) { 1148 m_postProcessor.init(); 1149 } 1150 1151 // build the query for getting the results 1152 SolrQuery queryForResults = query.clone(); 1153 // we add an additional filter, such that we can only find the documents we want to retrieve, as we figured out in the check query. 1154 if (!resultSolrIds.isEmpty()) { 1155 String queryFilterString = resultSolrIds.stream().collect(Collectors.joining(",")); 1156 queryForResults.addFilterQuery( 1157 "{!terms f=" + CmsSearchField.FIELD_SOLR_ID + " separator=\",\"}" + queryFilterString); 1158 } 1159 queryForResults.setRows(Integer.valueOf(resultSolrIds.size())); 1160 queryForResults.setStart(Integer.valueOf(0)); 1161 1162 if (LOG.isDebugEnabled()) { 1163 LOG.debug( 1164 Messages.get().getBundle().key(Messages.LOG_SOLR_DEBUG_RESULT_QUERY_2, queryForResults, getName())); 1165 } 1166 // perform the result query. 1167 solrResultTime = System.currentTimeMillis(); 1168 QueryResponse resultQueryResponse = m_solr.query(getCoreName(), queryForResults); 1169 solrResultTime = System.currentTimeMillis() - solrResultTime; 1170 1171 // List containing solr ids of filtered contents for which highlighting has to be removed. 1172 // Since we checked permissions just a few milliseconds ago, this should typically stay empty. 1173 List<String> filteredResultIds = new ArrayList<>(5); 1174 1175 for (SolrDocument doc : resultQueryResponse.getResults()) { 1176 try { 1177 CmsSolrDocument searchDoc = new CmsSolrDocument(doc); 1178 if (needsPermissionCheck(searchDoc)) { 1179 CmsResource resource = filter == null 1180 ? getResource(searchCms, searchDoc) 1181 : getResource(searchCms, searchDoc, filter); 1182 if (null != resource) { 1183 if (m_postProcessor != null) { 1184 doc = m_postProcessor.process( 1185 searchCms, 1186 resource, 1187 (SolrInputDocument)searchDoc.getDocument()); 1188 } 1189 resourceDocumentList.add(new CmsSearchResource(resource, searchDoc)); 1190 solrDocumentList.add(doc); 1191 } else { 1192 filteredResultIds.add(searchDoc.getFieldValueAsString(CmsSearchField.FIELD_SOLR_ID)); 1193 } 1194 } else { // should not happen unless the index has changed since the first query. 1195 resourceDocumentList.add(new CmsSearchResource(PSEUDO_RES, searchDoc)); 1196 solrDocumentList.add(doc); 1197 visibleHitCount--; 1198 } 1199 } catch (Exception e) { 1200 // should not happen, but if it does we want to go on with the next result nevertheless 1201 visibleHitCount--; 1202 LOG.warn(Messages.get().getBundle().key(Messages.LOG_SOLR_ERR_RESULT_ITERATION_FAILED_0), e); 1203 } 1204 } 1205 1206 long processTime = System.currentTimeMillis() - startTime - solrPermissionTime - solrResultTime; 1207 1208 //////////////////////////////////////////////////////////////////////////////////////////////////////////// CREATE 1209 /// THE FINAL RESPONSE 1210 1211 // we are manipulating the checkQueryResponse to set up the final response, we want to deliver. 1212 1213 // adjust start, max score and hit count displayed in the result list. 1214 solrDocumentList.setStart(start); 1215 Float finalMaxScore = sortByScoreDesc 1216 ? Float.valueOf(maxScore) 1217 : checkQueryResponse.getResults().getMaxScore(); 1218 solrDocumentList.setMaxScore(finalMaxScore); 1219 solrDocumentList.setNumFound(visibleHitCount); 1220 1221 // Exchange the search parameters in the response header by the ones from the (adjusted) original query. 1222 NamedList<Object> params = ((NamedList<Object>)(checkQueryResponse.getHeader().get(HEADER_PARAMS_NAME))); 1223 params.clear(); 1224 for (String paramName : query.getParameterNames()) { 1225 params.add(paramName, query.get(paramName)); 1226 } 1227 1228 // Fill in the documents to return. 1229 checkQueryResponse.getResponse().setVal( 1230 checkQueryResponse.getResponse().indexOf(QUERY_RESPONSE_NAME, 0), 1231 solrDocumentList); 1232 1233 // Fill in the time, the overall query took, including processing and permission check. 1234 ((NamedList<Object>)checkQueryResponse.getResponseHeader()).setVal( 1235 checkQueryResponse.getResponseHeader().indexOf(QUERY_TIME_NAME, 0), 1236 Integer.valueOf(Long.valueOf(System.currentTimeMillis() - startTime).intValue())); 1237 1238 // Fill in the highlighting information from the result query. 1239 if (query.getHighlight()) { 1240 NamedList<Object> highlighting = (NamedList<Object>)resultQueryResponse.getResponse().get( 1241 QUERY_HIGHLIGHTING_NAME); 1242 // filter out highlighting for documents where access is not permitted. 1243 for (String filteredId : filteredResultIds) { 1244 highlighting.remove(filteredId); 1245 } 1246 NamedList<Object> completeResponse = new SimpleOrderedMap<Object>(1); 1247 completeResponse.addAll(checkQueryResponse.getResponse()); 1248 completeResponse.add(QUERY_HIGHLIGHTING_NAME, highlighting); 1249 checkQueryResponse.setResponse(completeResponse); 1250 } 1251 1252 // build the result 1253 result = new CmsSolrResultList( 1254 query, 1255 checkQueryResponse, 1256 solrDocumentList, 1257 resourceDocumentList, 1258 start, 1259 Integer.valueOf(rows), 1260 Math.min(end, (start + solrDocumentList.size())), 1261 rows > 0 ? (start / rows) + 1 : 0, //page - but matches only in case of equally sized pages and is zero for rows=0 (because this was this way before!?!) 1262 visibleHitCount, 1263 finalMaxScore, 1264 startTime, 1265 System.currentTimeMillis()); 1266 if (LOG.isDebugEnabled()) { 1267 Object[] logParams = new Object[] { 1268 Long.valueOf(System.currentTimeMillis() - startTime), 1269 Long.valueOf(result.getNumFound()), 1270 Long.valueOf(solrPermissionTime + solrResultTime), 1271 Long.valueOf(processTime), 1272 Long.valueOf(result.getHighlightEndTime() != 0 ? result.getHighlightEndTime() - startTime : 0)}; 1273 LOG.debug( 1274 query.toString() 1275 + "\n" 1276 + Messages.get().getBundle().key(Messages.LOG_SOLR_SEARCH_EXECUTED_5, logParams)); 1277 } 1278 // write the response for the handler 1279 if (response != null) { 1280 // create and return the result 1281 core = m_solr instanceof EmbeddedSolrServer 1282 ? ((EmbeddedSolrServer)m_solr).getCoreContainer().getCore(getCoreName()) 1283 : null; 1284 1285 solrQueryRequest = new LocalSolrQueryRequest(core, query); 1286 SolrQueryResponse solrQueryResponse = new SolrQueryResponse(); 1287 solrQueryResponse.setAllValues(checkQueryResponse.getResponse()); 1288 writeResp(response, solrQueryRequest, solrQueryResponse); 1289 } 1290 } catch ( 1291 1292 Exception e) { 1293 throw new CmsSearchException( 1294 Messages.get().container( 1295 Messages.LOG_SOLR_ERR_SEARCH_EXECUTION_FAILD_1, 1296 CmsEncoder.decode(query.toString()), 1297 e), 1298 e); 1299 } finally { 1300 if (solrQueryRequest != null) { 1301 solrQueryRequest.close(); 1302 } 1303 if (null != core) { 1304 core.close(); 1305 } 1306 // re-set thread to previous priority 1307 Thread.currentThread().setPriority(previousPriority); 1308 } 1309 return result; 1310 } 1311 1312 /** 1313 * Default search method.<p> 1314 * 1315 * @param cms the current CMS object 1316 * @param query the query 1317 * 1318 * @return the results 1319 * 1320 * @throws CmsSearchException if something goes wrong 1321 * 1322 * @see #search(CmsObject, String) 1323 */ 1324 public CmsSolrResultList search(CmsObject cms, SolrQuery query) throws CmsSearchException { 1325 1326 return search(cms, CmsEncoder.decode(query.toString())); 1327 } 1328 1329 /** 1330 * Performs a search.<p> 1331 * 1332 * @param cms the cms object 1333 * @param solrQuery the Solr query 1334 * 1335 * @return a list of documents 1336 * 1337 * @throws CmsSearchException if something goes wrong 1338 * 1339 * @see #search(CmsObject, CmsSolrQuery, boolean) 1340 */ 1341 public CmsSolrResultList search(CmsObject cms, String solrQuery) throws CmsSearchException { 1342 1343 return search(cms, new CmsSolrQuery(null, CmsRequestUtil.createParameterMap(solrQuery)), false); 1344 } 1345 1346 /** 1347 * Writes the response into the writer.<p> 1348 * 1349 * NOTE: Currently not available for HTTP server.<p> 1350 * 1351 * @param response the servlet response 1352 * @param cms the CMS object to use for search 1353 * @param query the Solr query 1354 * @param ignoreMaxRows if to return unlimited results 1355 * 1356 * @throws Exception if there is no embedded server 1357 */ 1358 public void select(ServletResponse response, CmsObject cms, CmsSolrQuery query, boolean ignoreMaxRows) 1359 throws Exception { 1360 1361 throwExceptionIfSafetyRestrictionsAreViolated(cms, query, false); 1362 boolean isOnline = cms.getRequestContext().getCurrentProject().isOnlineProject(); 1363 CmsResourceFilter filter = isOnline ? null : CmsResourceFilter.IGNORE_EXPIRATION; 1364 1365 search(cms, query, ignoreMaxRows, response, false, filter); 1366 } 1367 1368 /** 1369 * Sets the document transformer.<p> 1370 * 1371 * @param documentTransformer the document transformer to set 1372 */ 1373 public void setDocumentTransformer(I_CmsSolrDocumentTransformer documentTransformer) { 1374 1375 documentTransformer.init(this); 1376 m_documentTransformer = documentTransformer; 1377 } 1378 1379 /** 1380 * Sets the logical key/name of this search index.<p> 1381 * 1382 * @param name the logical key/name of this search index 1383 * 1384 * @throws CmsIllegalArgumentException if the given name is null, empty or already taken by another search index 1385 */ 1386 @Override 1387 public void setName(String name) throws CmsIllegalArgumentException { 1388 1389 super.setName(name); 1390 updateCoreName(); 1391 } 1392 1393 /** 1394 * Sets the search post processor.<p> 1395 * 1396 * @param postProcessor the search post processor to set 1397 */ 1398 public void setPostProcessor(I_CmsSolrPostSearchProcessor postProcessor) { 1399 1400 m_postProcessor = postProcessor; 1401 } 1402 1403 /** 1404 * Sets the Solr server used by this index.<p> 1405 * 1406 * @param client the server to set 1407 */ 1408 public void setSolrServer(SolrClient client) { 1409 1410 m_solr = client; 1411 } 1412 1413 /** 1414 * Executes a spell checking Solr query and returns the Solr query response.<p> 1415 * 1416 * @param res the servlet response 1417 * @param cms the CMS object 1418 * @param q the query 1419 * 1420 * @throws CmsSearchException if something goes wrong 1421 */ 1422 public void spellCheck(ServletResponse res, CmsObject cms, CmsSolrQuery q) throws CmsSearchException { 1423 1424 throwExceptionIfSafetyRestrictionsAreViolated(cms, q, true); 1425 SolrCore core = null; 1426 LocalSolrQueryRequest solrQueryRequest = null; 1427 try { 1428 q.setRequestHandler("/spell"); 1429 q.setRows(Integer.valueOf(0)); 1430 1431 QueryResponse queryResponse = m_solr.query(getCoreName(), q); 1432 1433 List<CmsSearchResource> resourceDocumentList = new ArrayList<CmsSearchResource>(); 1434 SolrDocumentList solrDocumentList = new SolrDocumentList(); 1435 if (m_postProcessor != null) { 1436 for (int i = 0; (i < queryResponse.getResults().size()); i++) { 1437 try { 1438 SolrDocument doc = queryResponse.getResults().get(i); 1439 CmsSolrDocument searchDoc = new CmsSolrDocument(doc); 1440 if (needsPermissionCheck(searchDoc)) { 1441 // only if the document is an OpenCms internal resource perform the permission check 1442 CmsResource resource = getResource(cms, searchDoc); 1443 if (resource != null) { 1444 // permission check performed successfully: the user has read permissions! 1445 if (m_postProcessor != null) { 1446 doc = m_postProcessor.process( 1447 cms, 1448 resource, 1449 (SolrInputDocument)searchDoc.getDocument()); 1450 } 1451 resourceDocumentList.add(new CmsSearchResource(resource, searchDoc)); 1452 solrDocumentList.add(doc); 1453 } 1454 } 1455 } catch (Exception e) { 1456 // should not happen, but if it does we want to go on with the next result nevertheless 1457 LOG.warn(Messages.get().getBundle().key(Messages.LOG_SOLR_ERR_RESULT_ITERATION_FAILED_0), e); 1458 } 1459 } 1460 queryResponse.getResponse().setVal( 1461 queryResponse.getResponse().indexOf(QUERY_RESPONSE_NAME, 0), 1462 solrDocumentList); 1463 } 1464 1465 // create and return the result 1466 core = m_solr instanceof EmbeddedSolrServer 1467 ? ((EmbeddedSolrServer)m_solr).getCoreContainer().getCore(getCoreName()) 1468 : null; 1469 1470 SolrQueryResponse solrQueryResponse = new SolrQueryResponse(); 1471 solrQueryResponse.setAllValues(queryResponse.getResponse()); 1472 1473 // create and initialize the solr request 1474 solrQueryRequest = new LocalSolrQueryRequest(core, solrQueryResponse.getResponseHeader()); 1475 // set the OpenCms Solr query as parameters to the request 1476 solrQueryRequest.setParams(q); 1477 1478 writeResp(res, solrQueryRequest, solrQueryResponse); 1479 1480 } catch (Exception e) { 1481 throw new CmsSearchException( 1482 Messages.get().container(Messages.LOG_SOLR_ERR_SEARCH_EXECUTION_FAILD_1, q), 1483 e); 1484 } finally { 1485 if (solrQueryRequest != null) { 1486 solrQueryRequest.close(); 1487 } 1488 if (core != null) { 1489 core.close(); 1490 } 1491 } 1492 } 1493 1494 /** 1495 * @see org.opencms.search.CmsSearchIndex#createIndexBackup() 1496 */ 1497 @Override 1498 protected String createIndexBackup() { 1499 1500 if (!isBackupReindexing()) { 1501 // if no backup is generated we don't need to do anything 1502 return null; 1503 } 1504 if (m_solr instanceof EmbeddedSolrServer) { 1505 EmbeddedSolrServer ser = (EmbeddedSolrServer)m_solr; 1506 CoreContainer con = ser.getCoreContainer(); 1507 SolrCore core = con.getCore(getCoreName()); 1508 if (core != null) { 1509 try { 1510 SolrRequestHandler h = core.getRequestHandler("/replication"); 1511 if (h instanceof ReplicationHandler) { 1512 h.handleRequest( 1513 new LocalSolrQueryRequest(core, CmsRequestUtil.createParameterMap("?command=backup")), 1514 new SolrQueryResponse()); 1515 } 1516 } finally { 1517 core.close(); 1518 } 1519 } 1520 } 1521 return null; 1522 } 1523 1524 /** 1525 * Check, if the current user has permissions on the document's resource. 1526 * @param cms the context 1527 * @param doc the solr document (from the search result) 1528 * @param filter the resource filter to use for checking permissions 1529 * @return <code>true</code> iff the resource mirrored by the search result can be read by the current user. 1530 */ 1531 protected boolean hasPermissions(CmsObject cms, CmsSolrDocument doc, CmsResourceFilter filter) { 1532 1533 return null != (filter == null ? getResource(cms, doc) : getResource(cms, doc, filter)); 1534 } 1535 1536 /** 1537 * @see org.opencms.search.CmsSearchIndex#indexSearcherClose() 1538 */ 1539 @SuppressWarnings("sync-override") 1540 @Override 1541 protected void indexSearcherClose() { 1542 1543 // nothing to do here 1544 } 1545 1546 /** 1547 * @see org.opencms.search.CmsSearchIndex#indexSearcherOpen(java.lang.String) 1548 */ 1549 @SuppressWarnings("sync-override") 1550 @Override 1551 protected void indexSearcherOpen(final String path) { 1552 1553 // nothing to do here 1554 } 1555 1556 /** 1557 * @see org.opencms.search.CmsSearchIndex#indexSearcherUpdate() 1558 */ 1559 @SuppressWarnings("sync-override") 1560 @Override 1561 protected void indexSearcherUpdate() { 1562 1563 // nothing to do here 1564 } 1565 1566 /** 1567 * Checks if the current user is allowed to access non-online indexes.<p> 1568 * 1569 * To access non-online indexes the current user must be a workplace user at least.<p> 1570 * 1571 * @param cms the CMS object initialized with the current request context / user 1572 * 1573 * @throws CmsSearchException thrown if the access is not permitted 1574 */ 1575 private void checkOfflineAccess(CmsObject cms) throws CmsSearchException { 1576 1577 // If an offline index is being selected, check permissions 1578 if (!CmsProject.ONLINE_PROJECT_NAME.equals(getProject())) { 1579 // only if the user has the role Workplace user, he is allowed to access the Offline index 1580 try { 1581 OpenCms.getRoleManager().checkRole(cms, CmsRole.ELEMENT_AUTHOR); 1582 } catch (CmsRoleViolationException e) { 1583 throw new CmsSearchException( 1584 Messages.get().container( 1585 Messages.LOG_SOLR_ERR_SEARCH_PERMISSION_VIOLATION_2, 1586 getName(), 1587 cms.getRequestContext().getCurrentUser()), 1588 e); 1589 } 1590 } 1591 } 1592 1593 /** 1594 * Generates a valid core name from the provided name (the index name). 1595 * @param name the index name. 1596 * @return the core name 1597 */ 1598 private String generateCoreName(final String name) { 1599 1600 if (name != null) { 1601 return name.replace(" ", "-"); 1602 } 1603 return null; 1604 } 1605 1606 /** 1607 * Checks if the query should be executed using the debug mode where the security restrictions do not apply. 1608 * @param cms the current context. 1609 * @param query the query to execute. 1610 * @return a flag, indicating, if the query should be performed in debug mode. 1611 */ 1612 private boolean isDebug(CmsObject cms, CmsSolrQuery query) { 1613 1614 String[] debugSecretValues = query.remove(REQUEST_PARAM_DEBUG_SECRET); 1615 String debugSecret = (debugSecretValues == null) || (debugSecretValues.length < 1) 1616 ? null 1617 : debugSecretValues[0]; 1618 if ((null != debugSecret) && !debugSecret.trim().isEmpty() && (null != m_handlerDebugSecretFile)) { 1619 try { 1620 CmsFile secretFile = cms.readFile(m_handlerDebugSecretFile); 1621 String secret = new String(secretFile.getContents(), CmsFileUtil.getEncoding(cms, secretFile)); 1622 return secret.trim().equals(debugSecret.trim()); 1623 } catch (Exception e) { 1624 LOG.info( 1625 "Failed to read secret file for index \"" 1626 + getName() 1627 + "\" at path \"" 1628 + m_handlerDebugSecretFile 1629 + "\"."); 1630 } 1631 } 1632 return false; 1633 } 1634 1635 /** 1636 * Throws an exception if the request can for security reasons not be performed. 1637 * Security restrictions can be set via parameters of the index. 1638 * 1639 * @param cms the current context. 1640 * @param query the query. 1641 * @param isSpell flag, indicating if the spellcheck handler is requested. 1642 * @throws CmsSearchException thrown if the query cannot be executed due to security reasons. 1643 */ 1644 private void throwExceptionIfSafetyRestrictionsAreViolated(CmsObject cms, CmsSolrQuery query, boolean isSpell) 1645 throws CmsSearchException { 1646 1647 if (!isDebug(cms, query)) { 1648 if (isSpell) { 1649 if (m_handlerSpellDisabled) { 1650 throw new CmsSearchException(Messages.get().container(Messages.GUI_HANDLER_REQUEST_NOT_ALLOWED_0)); 1651 } 1652 } else { 1653 if (m_handlerSelectDisabled) { 1654 throw new CmsSearchException(Messages.get().container(Messages.GUI_HANDLER_REQUEST_NOT_ALLOWED_0)); 1655 } 1656 int start = null != query.getStart() ? query.getStart().intValue() : 0; 1657 int rows = null != query.getRows() ? query.getRows().intValue() : CmsSolrQuery.DEFAULT_ROWS.intValue(); 1658 if ((m_handlerMaxAllowedResultsAtAll >= 0) && ((rows + start) > m_handlerMaxAllowedResultsAtAll)) { 1659 throw new CmsSearchException( 1660 Messages.get().container( 1661 Messages.GUI_HANDLER_TOO_MANY_RESULTS_REQUESTED_AT_ALL_2, 1662 Integer.valueOf(m_handlerMaxAllowedResultsAtAll), 1663 Integer.valueOf(rows + start))); 1664 } 1665 if ((m_handlerMaxAllowedResultsPerPage >= 0) && (rows > m_handlerMaxAllowedResultsPerPage)) { 1666 throw new CmsSearchException( 1667 Messages.get().container( 1668 Messages.GUI_HANDLER_TOO_MANY_RESULTS_REQUESTED_PER_PAGE_2, 1669 Integer.valueOf(m_handlerMaxAllowedResultsPerPage), 1670 Integer.valueOf(rows))); 1671 } 1672 if ((null != m_handlerAllowedFields) && (Stream.of(m_handlerAllowedFields).anyMatch(x -> true))) { 1673 if (query.getFields().equals(CmsSolrQuery.ALL_RETURN_FIELDS)) { 1674 query.setFields(m_handlerAllowedFields); 1675 } else { 1676 for (String requestedField : query.getFields().split(",")) { 1677 if (Stream.of(m_handlerAllowedFields).noneMatch( 1678 allowedField -> allowedField.equals(requestedField))) { 1679 throw new CmsSearchException( 1680 Messages.get().container( 1681 Messages.GUI_HANDLER_REQUESTED_FIELD_NOT_ALLOWED_2, 1682 requestedField, 1683 Stream.of(m_handlerAllowedFields).reduce("", (a, b) -> a + "," + b))); 1684 } 1685 } 1686 } 1687 } 1688 } 1689 } 1690 } 1691 1692 /** 1693 * Updates the core name to be in sync with the index name. 1694 */ 1695 private void updateCoreName() { 1696 1697 m_coreName = generateCoreName(getName()); 1698 1699 } 1700 1701 /** 1702 * Writes the Solr response.<p> 1703 * 1704 * @param response the servlet response 1705 * @param queryRequest the Solr request 1706 * @param queryResponse the Solr response to write 1707 * 1708 * @throws IOException if sth. goes wrong 1709 * @throws UnsupportedEncodingException if sth. goes wrong 1710 */ 1711 private void writeResp(ServletResponse response, SolrQueryRequest queryRequest, SolrQueryResponse queryResponse) 1712 throws IOException, UnsupportedEncodingException { 1713 1714 if (m_solr instanceof EmbeddedSolrServer) { 1715 SolrCore core = ((EmbeddedSolrServer)m_solr).getCoreContainer().getCore(getCoreName()); 1716 Writer out = null; 1717 try { 1718 QueryResponseWriter responseWriter = core.getQueryResponseWriter(queryRequest); 1719 1720 final String ct = responseWriter.getContentType(queryRequest, queryResponse); 1721 if (null != ct) { 1722 response.setContentType(ct); 1723 } 1724 1725 responseWriter.write(response.getOutputStream(), queryRequest, queryResponse); 1726 } finally { 1727 core.close(); 1728 if (out != null) { 1729 out.close(); 1730 } 1731 } 1732 } else { 1733 throw new UnsupportedOperationException(); 1734 } 1735 } 1736}