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