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