001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.search; 029 030import org.opencms.ade.containerpage.CmsDetailOnlyContainerUtil; 031import org.opencms.configuration.CmsConfigurationException; 032import org.opencms.configuration.CmsParameterConfiguration; 033import org.opencms.db.CmsDriverManager; 034import org.opencms.db.CmsModificationContext; 035import org.opencms.db.CmsPublishedResource; 036import org.opencms.db.CmsResourceState; 037import org.opencms.file.CmsObject; 038import org.opencms.file.CmsProject; 039import org.opencms.file.CmsResource; 040import org.opencms.file.CmsResourceFilter; 041import org.opencms.file.CmsUser; 042import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 043import org.opencms.file.types.CmsResourceTypeXmlContent; 044import org.opencms.file.types.I_CmsResourceType; 045import org.opencms.i18n.CmsLocaleManager; 046import org.opencms.i18n.CmsMessageContainer; 047import org.opencms.loader.CmsLoaderException; 048import org.opencms.loader.CmsResourceManager; 049import org.opencms.main.CmsBroadcast.ContentMode; 050import org.opencms.main.CmsEvent; 051import org.opencms.main.CmsException; 052import org.opencms.main.CmsIllegalArgumentException; 053import org.opencms.main.CmsIllegalStateException; 054import org.opencms.main.CmsLog; 055import org.opencms.main.I_CmsEventListener; 056import org.opencms.main.OpenCms; 057import org.opencms.relations.CmsRelation; 058import org.opencms.relations.CmsRelationFilter; 059import org.opencms.relations.CmsRelationType; 060import org.opencms.report.CmsLogReport; 061import org.opencms.report.CmsShellLogReport; 062import org.opencms.report.I_CmsReport; 063import org.opencms.scheduler.I_CmsScheduledJob; 064import org.opencms.search.documents.A_CmsVfsDocument; 065import org.opencms.search.documents.CmsExtractionResultCache; 066import org.opencms.search.documents.I_CmsDocumentFactory; 067import org.opencms.search.documents.I_CmsTermHighlighter; 068import org.opencms.search.fields.CmsLuceneField; 069import org.opencms.search.fields.CmsLuceneFieldConfiguration; 070import org.opencms.search.fields.CmsSearchField; 071import org.opencms.search.fields.CmsSearchFieldConfiguration; 072import org.opencms.search.fields.CmsSearchFieldMapping; 073import org.opencms.search.fields.I_CmsSearchFieldConfiguration; 074import org.opencms.search.solr.CmsSolrConfiguration; 075import org.opencms.search.solr.CmsSolrFieldConfiguration; 076import org.opencms.search.solr.CmsSolrIndex; 077import org.opencms.search.solr.I_CmsSolrIndexWriter; 078import org.opencms.search.solr.spellchecking.CmsSolrSpellchecker; 079import org.opencms.search.solr.spellchecking.CmsSpellcheckDictionaryIndexer; 080import org.opencms.security.CmsRole; 081import org.opencms.security.CmsRoleViolationException; 082import org.opencms.util.A_CmsModeStringEnumeration; 083import org.opencms.util.CmsFileUtil; 084import org.opencms.util.CmsPriorityLock; 085import org.opencms.util.CmsSecretUtil; 086import org.opencms.util.CmsStringUtil; 087import org.opencms.util.CmsUUID; 088import org.opencms.util.CmsWaitHandle; 089 090import java.io.File; 091import java.io.IOException; 092import java.net.URI; 093import java.net.URISyntaxException; 094import java.nio.file.FileSystems; 095import java.nio.file.Paths; 096import java.util.ArrayList; 097import java.util.Collection; 098import java.util.Collections; 099import java.util.HashMap; 100import java.util.HashSet; 101import java.util.Iterator; 102import java.util.LinkedHashMap; 103import java.util.List; 104import java.util.ListIterator; 105import java.util.Locale; 106import java.util.Map; 107import java.util.Map.Entry; 108import java.util.Set; 109import java.util.TreeMap; 110import java.util.concurrent.LinkedBlockingQueue; 111import java.util.concurrent.ScheduledFuture; 112import java.util.concurrent.ScheduledThreadPoolExecutor; 113import java.util.concurrent.ThreadPoolExecutor; 114import java.util.concurrent.TimeUnit; 115import java.util.stream.Collectors; 116 117import org.apache.commons.logging.Log; 118import org.apache.lucene.analysis.Analyzer; 119import org.apache.lucene.analysis.CharArraySet; 120import org.apache.lucene.analysis.standard.StandardAnalyzer; 121import org.apache.solr.client.solrj.SolrClient; 122import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer; 123import org.apache.solr.client.solrj.impl.HttpJdkSolrClient.Builder; 124import org.apache.solr.core.CoreContainer; 125import org.apache.solr.core.CoreDescriptor; 126import org.apache.solr.core.SolrCore; 127 128import com.google.common.util.concurrent.ThreadFactoryBuilder; 129 130/** 131 * Implements the general management and configuration of the search and 132 * indexing facilities in OpenCms.<p> 133 * 134 * @since 6.0.0 135 */ 136public class CmsSearchManager implements I_CmsScheduledJob, I_CmsEventListener { 137 138 /** 139 * Enumeration class for force unlock types.<p> 140 */ 141 public static final class CmsSearchForceUnlockMode extends A_CmsModeStringEnumeration { 142 143 /** Force unlock type "always". */ 144 public static final CmsSearchForceUnlockMode ALWAYS = new CmsSearchForceUnlockMode("always"); 145 146 /** Force unlock type "never". */ 147 public static final CmsSearchForceUnlockMode NEVER = new CmsSearchForceUnlockMode("never"); 148 149 /** Force unlock type "only full". */ 150 public static final CmsSearchForceUnlockMode ONLYFULL = new CmsSearchForceUnlockMode("onlyfull"); 151 152 /** Serializable version id. */ 153 private static final long serialVersionUID = 74746076708908673L; 154 155 /** 156 * Creates a new force unlock type with the given name.<p> 157 * 158 * @param mode the mode id to use 159 */ 160 protected CmsSearchForceUnlockMode(String mode) { 161 162 super(mode); 163 } 164 165 /** 166 * Returns the lock type for the given type value.<p> 167 * 168 * @param type the type value to get the lock type for 169 * 170 * @return the lock type for the given type value 171 */ 172 public static CmsSearchForceUnlockMode valueOf(String type) { 173 174 if (type.equals(ALWAYS.toString())) { 175 return ALWAYS; 176 } else if (type.equals(NEVER.toString())) { 177 return NEVER; 178 } else { 179 return ONLYFULL; 180 } 181 } 182 } 183 184 /** 185 * Handles offline index generation.<p> 186 */ 187 protected class CmsSearchOfflineHandler implements I_CmsEventListener { 188 189 /** Indicates if the event handlers for the offline search have been already registered. */ 190 private boolean m_isEventRegistered; 191 192 /** The list of resources to index. */ 193 private List<CmsPublishedResource> m_resourcesToIndex; 194 195 /** 196 * Initializes the offline index handler.<p> 197 */ 198 protected CmsSearchOfflineHandler() { 199 200 m_resourcesToIndex = new ArrayList<CmsPublishedResource>(); 201 } 202 203 /** 204 * Implements the event listener of this class.<p> 205 * 206 * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) 207 */ 208 @SuppressWarnings("unchecked") 209 public void cmsEvent(CmsEvent event) { 210 211 Object change = event.getData().get(I_CmsEventListener.KEY_CHANGE); 212 switch (event.getType()) { 213 case I_CmsEventListener.EVENT_PROPERTY_MODIFIED: 214 case I_CmsEventListener.EVENT_RESOURCE_CREATED: 215 case I_CmsEventListener.EVENT_RESOURCE_AND_PROPERTIES_MODIFIED: 216 case I_CmsEventListener.EVENT_RESOURCE_MODIFIED: 217 if ((change != null) && change.equals(Integer.valueOf(CmsDriverManager.NOTHING_CHANGED))) { 218 // skip lock & unlock 219 return; 220 } 221 // skip indexing if flag is set in event 222 Object skip = event.getData().get(I_CmsEventListener.KEY_SKIPINDEX); 223 if (skip != null) { 224 return; 225 } 226 227 // a resource has been modified - offline indexes require (re)indexing 228 List<CmsResource> resources = Collections.singletonList( 229 (CmsResource)event.getData().get(I_CmsEventListener.KEY_RESOURCE)); 230 reIndexResources(resources); 231 break; 232 case I_CmsEventListener.EVENT_RESOURCE_DELETED: 233 List<CmsResource> eventResources = (List<CmsResource>)event.getData().get( 234 I_CmsEventListener.KEY_RESOURCES); 235 List<CmsResource> resourcesToDelete = new ArrayList<CmsResource>(eventResources); 236 for (CmsResource res : resourcesToDelete) { 237 if (res.getState().isNew()) { 238 // if the resource is new and a delete action was performed 239 // --> set the state of the resource to deleted 240 res.setState(CmsResourceState.STATE_DELETED); 241 } 242 } 243 reIndexResources(resourcesToDelete); 244 break; 245 case I_CmsEventListener.EVENT_RESOURCES_AND_PROPERTIES_MODIFIED: 246 if (I_CmsEventListener.VALUE_CREATE_SIBLING.equals(change)) { 247 List<CmsResource> resList = (List<CmsResource>)event.getData().get( 248 I_CmsEventListener.KEY_RESOURCES); 249 if ((resList != null) && (resList.size() >= 3)) { 250 System.out.println("Sibling creation case, resource = " + resList.get(1).getRootPath()); 251 reIndexResources(Collections.singletonList(resList.get(1))); 252 253 } 254 } else { 255 reIndexResources((List<CmsResource>)event.getData().get(I_CmsEventListener.KEY_RESOURCES)); 256 } 257 break; 258 case I_CmsEventListener.EVENT_RESOURCE_MOVED: 259 case I_CmsEventListener.EVENT_RESOURCE_COPIED: 260 case I_CmsEventListener.EVENT_RESOURCES_MODIFIED: 261 262 // a list of resources has been modified - offline indexes require (re)indexing 263 reIndexResources((List<CmsResource>)event.getData().get(I_CmsEventListener.KEY_RESOURCES)); 264 break; 265 default: 266 // no operation 267 } 268 } 269 270 /** 271 * Adds a list of {@link CmsPublishedResource} objects to be indexed.<p> 272 * 273 * @param resourcesToIndex the list of {@link CmsPublishedResource} objects to be indexed 274 */ 275 protected synchronized void addResourcesToIndex(List<CmsPublishedResource> resourcesToIndex) { 276 277 m_resourcesToIndex.addAll(resourcesToIndex); 278 } 279 280 /** 281 * Returns the list of {@link CmsPublishedResource} objects to index.<p> 282 * 283 * @return the resources to index 284 */ 285 protected List<CmsPublishedResource> getResourcesToIndex() { 286 287 List<CmsPublishedResource> result; 288 synchronized (this) { 289 result = m_resourcesToIndex; 290 m_resourcesToIndex = new ArrayList<CmsPublishedResource>(); 291 } 292 try { 293 CmsObject cms = m_adminCms; 294 CmsProject offline = getOfflineIndexProject(); 295 if (offline != null) { 296 // switch to the offline project if available 297 cms = OpenCms.initCmsObject(m_adminCms); 298 cms.getRequestContext().setCurrentProject(offline); 299 } 300 addAdditionallyAffectedResources(cms, result); 301 } catch (CmsException e) { 302 LOG.error(e.getLocalizedMessage(), e); 303 } 304 return result; 305 } 306 307 /** 308 * Initializes this offline search handler, registering the event handlers if required.<p> 309 */ 310 protected void initialize() { 311 312 if (m_offlineIndexes.size() > 0) { 313 // there is at least one offline index configured 314 if ((m_offlineIndexThread == null) || !m_offlineIndexThread.isAlive()) { 315 // create the offline indexing thread 316 m_offlineIndexThread = new CmsSearchOfflineIndexThread(this); 317 // start the offline index thread 318 m_offlineIndexThread.start(); 319 } 320 } else { 321 if ((m_offlineIndexThread != null) && m_offlineIndexThread.isAlive()) { 322 // no offline indexes but thread still running, stop the thread 323 m_offlineIndexThread.shutDown(); 324 m_offlineIndexThread = null; 325 } 326 } 327 // do this only in case there are offline indexes configured 328 if (!m_isEventRegistered && (m_offlineIndexes.size() > 0)) { 329 m_isEventRegistered = true; 330 // register this object as event listener 331 OpenCms.addCmsEventListener( 332 this, 333 new int[] { 334 I_CmsEventListener.EVENT_PROPERTY_MODIFIED, 335 I_CmsEventListener.EVENT_RESOURCE_CREATED, 336 I_CmsEventListener.EVENT_RESOURCE_AND_PROPERTIES_MODIFIED, 337 I_CmsEventListener.EVENT_RESOURCE_MODIFIED, 338 I_CmsEventListener.EVENT_RESOURCES_AND_PROPERTIES_MODIFIED, 339 I_CmsEventListener.EVENT_RESOURCE_MOVED, 340 I_CmsEventListener.EVENT_RESOURCE_DELETED, 341 I_CmsEventListener.EVENT_RESOURCE_COPIED, 342 I_CmsEventListener.EVENT_RESOURCES_MODIFIED}); 343 } 344 } 345 346 /** 347 * Updates all offline indexes for the given list of {@link CmsResource} objects.<p> 348 * 349 * @param resources a list of {@link CmsResource} objects to update in the offline indexes 350 */ 351 protected synchronized void reIndexResources(List<CmsResource> resources) { 352 353 List<CmsPublishedResource> resourcesToIndex = new ArrayList<CmsPublishedResource>(resources.size()); 354 for (CmsResource res : resources) { 355 CmsPublishedResource pubRes = new CmsPublishedResource(res); 356 resourcesToIndex.add(pubRes); 357 } 358 if (resourcesToIndex.size() > 0) { 359 // add the resources found to the offline index thread 360 addResourcesToIndex(resourcesToIndex); 361 } 362 } 363 } 364 365 /** 366 * The offline indexer thread runs periodically and indexes all resources added by the event handler.<p> 367 */ 368 protected class CmsSearchOfflineIndexThread extends Thread { 369 370 /** The event handler that triggers this thread. */ 371 CmsSearchOfflineHandler m_handler; 372 373 /** Indicates if this thread is still alive. */ 374 boolean m_isAlive; 375 376 /** Indicates that an index update thread is currently running. */ 377 private boolean m_isUpdating; 378 379 /** If true a manual update (after file upload) was triggered. */ 380 private boolean m_updateTriggered; 381 382 /** The wait handle used for signalling when the worker thread has finished. */ 383 private CmsWaitHandle m_waitHandle = new CmsWaitHandle(); 384 385 /** 386 * Constructor.<p> 387 * 388 * @param handler the offline index event handler 389 */ 390 protected CmsSearchOfflineIndexThread(CmsSearchOfflineHandler handler) { 391 392 super("OpenCms: Offline Search Indexer"); 393 m_handler = handler; 394 } 395 396 /** 397 * Gets the wait handle used for signalling when the worker thread has finished. 398 * 399 * @return the wait handle 400 */ 401 public CmsWaitHandle getWaitHandle() { 402 403 return m_waitHandle; 404 } 405 406 /** 407 * @see java.lang.Thread#interrupt() 408 */ 409 @Override 410 public void interrupt() { 411 412 super.interrupt(); 413 m_updateTriggered = true; 414 } 415 416 /** 417 * @see java.lang.Thread#run() 418 */ 419 @Override 420 public void run() { 421 422 // create a log report for the output 423 I_CmsReport report = new CmsLogReport(m_adminCms.getRequestContext().getLocale(), CmsSearchManager.class); 424 long offlineUpdateFrequency = getOfflineUpdateFrequency(); 425 m_updateTriggered = false; 426 try { 427 while (m_isAlive) { 428 if (!m_updateTriggered) { 429 try { 430 sleep(offlineUpdateFrequency); 431 } catch (InterruptedException e) { 432 // continue the thread after interruption 433 if (!m_isAlive) { 434 // the thread has been shut down while sleeping 435 continue; 436 } 437 if (offlineUpdateFrequency != getOfflineUpdateFrequency()) { 438 // offline update frequency change - clear interrupt status 439 offlineUpdateFrequency = getOfflineUpdateFrequency(); 440 } 441 LOG.info(e.getLocalizedMessage(), e); 442 } 443 } 444 if (m_isAlive) { 445 // set update trigger to false since we do the update now 446 m_updateTriggered = false; 447 // get list of resource to update 448 List<CmsPublishedResource> resourcesToIndex = getResourcesToIndex(); 449 if (resourcesToIndex.size() > 0) { 450 // only start indexing if there is at least one resource 451 startOfflineUpdateThread(report, resourcesToIndex); 452 } else { 453 getWaitHandle().release(); 454 } 455 // this is just called to clear the interrupt status of the thread 456 interrupted(); 457 } 458 } 459 } finally { 460 // make sure that live status is reset in case of Exceptions 461 m_isAlive = false; 462 } 463 464 } 465 466 /** 467 * @see java.lang.Thread#start() 468 */ 469 @Override 470 public synchronized void start() { 471 472 m_isAlive = true; 473 super.start(); 474 } 475 476 /** 477 * Obtains the list of resource to update in the offline index, 478 * then optimizes the list by removing duplicate entries.<p> 479 * 480 * @return the list of resource to update in the offline index 481 */ 482 protected List<CmsPublishedResource> getResourcesToIndex() { 483 484 List<CmsPublishedResource> resourcesToIndex = m_handler.getResourcesToIndex(); 485 List<CmsPublishedResource> result = new ArrayList<CmsPublishedResource>(resourcesToIndex.size()); 486 487 // Reverse to always keep the last list entries 488 Collections.reverse(resourcesToIndex); 489 for (CmsPublishedResource pubRes : resourcesToIndex) { 490 boolean addResource = true; 491 for (CmsPublishedResource resRes : result) { 492 if (pubRes.equals(resRes) 493 && (pubRes.getState() == resRes.getState()) 494 && (pubRes.getMovedState() == resRes.getMovedState()) 495 && pubRes.getRootPath().equals(resRes.getRootPath())) { 496 // resource already in the update list 497 addResource = false; 498 break; 499 } 500 } 501 if (addResource) { 502 result.add(pubRes); 503 } 504 505 } 506 Collections.reverse(result); 507 return changeStateOfMoveOriginsToDeleted(result); 508 } 509 510 /** 511 * Shuts down this offline index thread.<p> 512 */ 513 protected void shutDown() { 514 515 m_isAlive = false; 516 interrupt(); 517 if (m_isUpdating) { 518 long waitTime = getOfflineUpdateFrequency() / 2; 519 int waitSteps = 0; 520 do { 521 try { 522 // wait half the time of the offline index frequency for the thread to finish 523 Thread.sleep(waitTime); 524 } catch (InterruptedException e) { 525 // continue 526 LOG.info(e.getLocalizedMessage(), e); 527 } 528 waitSteps++; 529 // wait 5 times then stop waiting 530 } while ((waitSteps < 5) && m_isUpdating); 531 } 532 } 533 534 /** 535 * Updates the offline search indexes for the given list of resources.<p> 536 * 537 * @param report the report to write the index information to 538 * @param resourcesToIndex the list of {@link CmsPublishedResource} objects to index 539 */ 540 protected void startOfflineUpdateThread(I_CmsReport report, List<CmsPublishedResource> resourcesToIndex) { 541 542 CmsSearchOfflineIndexWorkThread thread = new CmsSearchOfflineIndexWorkThread(report, resourcesToIndex); 543 long startTime = System.currentTimeMillis(); 544 long waitTime = getOfflineUpdateFrequency() / 2; 545 if (LOG.isDebugEnabled()) { 546 LOG.debug( 547 Messages.get().getBundle().key( 548 Messages.LOG_OI_UPDATE_START_1, 549 Integer.valueOf(resourcesToIndex.size()))); 550 } 551 552 m_isUpdating = true; 553 thread.start(); 554 555 do { 556 try { 557 // wait half the time of the offline index frequency for the thread to finish 558 thread.join(waitTime); 559 } catch (InterruptedException e) { 560 // continue 561 LOG.info(e.getLocalizedMessage(), e); 562 } 563 if (thread.isAlive()) { 564 LOG.warn( 565 Messages.get().getBundle().key( 566 Messages.LOG_OI_UPDATE_LONG_2, 567 Integer.valueOf(resourcesToIndex.size()), 568 Long.valueOf(System.currentTimeMillis() - startTime))); 569 } 570 } while (thread.isAlive()); 571 m_isUpdating = false; 572 573 if (LOG.isDebugEnabled()) { 574 LOG.debug( 575 Messages.get().getBundle().key( 576 Messages.LOG_OI_UPDATE_FINISH_2, 577 Integer.valueOf(resourcesToIndex.size()), 578 Long.valueOf(System.currentTimeMillis() - startTime))); 579 } 580 } 581 582 /** 583 * Helper method which changes the states of resources which are to be indexed but have the wrong path to 'deleted'. 584 * This is needed to deal with moved resources, since the documents with the old paths must be removed from the index, 585 * 586 * @param resourcesToIndex the resources to index 587 * 588 * @return the resources to index, but resource states are set to 'deleted' for resources with outdated paths 589 */ 590 private List<CmsPublishedResource> changeStateOfMoveOriginsToDeleted( 591 List<CmsPublishedResource> resourcesToIndex) { 592 593 Map<CmsUUID, String> lastValidPaths = new HashMap<CmsUUID, String>(); 594 for (CmsPublishedResource resource : resourcesToIndex) { 595 if (resource.getState().isDeleted()) { 596 // we don't want the last path to be from a deleted resource 597 continue; 598 } 599 lastValidPaths.put(resource.getStructureId(), resource.getRootPath()); 600 } 601 List<CmsPublishedResource> result = new ArrayList<CmsPublishedResource>(); 602 for (CmsPublishedResource resource : resourcesToIndex) { 603 if (resource.getState().isDeleted()) { 604 result.add(resource); 605 continue; 606 } 607 String lastValidPath = lastValidPaths.get(resource.getStructureId()); 608 if (resource.getRootPath().equals(lastValidPath) || resource.getStructureId().isNullUUID()) { 609 result.add(resource); 610 } else { 611 result.add( 612 new CmsPublishedResource( 613 resource.getStructureId(), 614 resource.getResourceId(), 615 resource.getPublishTag(), 616 resource.getRootPath(), 617 resource.getType(), 618 resource.isFolder(), 619 CmsResource.STATE_DELETED, // make sure index entry with outdated path is deleted 620 resource.getSiblingCount())); 621 } 622 } 623 return result; 624 } 625 } 626 627 /** 628 * An offline index worker Thread runs each time for every offline index update action.<p> 629 * 630 * This was decoupled from the main {@link CmsSearchOfflineIndexThread} in order to avoid 631 * problems if a single operation "hangs" the Tread.<p> 632 */ 633 protected class CmsSearchOfflineIndexWorkThread extends Thread { 634 635 /** The report to write the index information to. */ 636 I_CmsReport m_report; 637 638 /** The list of {@link CmsPublishedResource} objects to index. */ 639 List<CmsPublishedResource> m_resourcesToIndex; 640 641 /** 642 * Updates the offline search indexes for the given list of resources.<p> 643 * 644 * @param report the report to write the index information to 645 * @param resourcesToIndex the list of {@link CmsPublishedResource} objects to index 646 */ 647 protected CmsSearchOfflineIndexWorkThread(I_CmsReport report, List<CmsPublishedResource> resourcesToIndex) { 648 649 super("OpenCms: Offline Search Index Worker"); 650 m_report = report; 651 m_resourcesToIndex = resourcesToIndex; 652 } 653 654 /** 655 * @see java.lang.Thread#run() 656 */ 657 @Override 658 public void run() { 659 660 updateIndexOffline(m_report, m_resourcesToIndex); 661 if (m_offlineIndexThread != null) { 662 m_offlineIndexThread.getWaitHandle().release(); 663 } 664 } 665 666 } 667 668 /** 669 * Helper class for batching resources arising from multiple independent 'instant publish' operations for indexing. 670 * <p>This is to reduce overhead for indexing, while still limiting the indexing batch size to not block 'interactive' publishing too much. 671 * <p>The batching is time-based, i.e. file changes in a given time span (currently 2 seconds) are collected and then indexed together. 672 * <p>However, changes that include publish resources with state 'deleted' (actual deletions or move operations) are never batched together with others, to avoid complications. 673 */ 674 protected class InstantPublishIndexingQueue { 675 676 /** Current map of batched resources to publish, grouped by id. */ 677 private Map<CmsUUID, List<CmsPublishedResource>> m_currentBatch = new HashMap<>(); 678 679 /** The task for flushing the queue. */ 680 private ScheduledFuture<?> m_flushTask; 681 682 /** The executor used to do the actual indexing. */ 683 private ThreadPoolExecutor m_executor; 684 685 private ScheduledThreadPoolExecutor m_flushExecutor; 686 687 private LinkedBlockingQueue<Runnable> m_workQueue = new LinkedBlockingQueue<>(); 688 689 /** 690 * Creates a new instance. 691 */ 692 public InstantPublishIndexingQueue() { 693 694 m_executor = new ThreadPoolExecutor( 695 0, 696 1, 697 10, 698 TimeUnit.SECONDS, 699 m_workQueue, 700 new ThreadFactoryBuilder().setNameFormat("instant-publish-indexer-%d").build()); 701 m_flushExecutor = new ScheduledThreadPoolExecutor( 702 1, 703 new ThreadFactoryBuilder().setNameFormat("instant-publish-flush-%d").build()); 704 } 705 706 /** 707 * Adds the resources from a publish job to the queue. 708 * 709 * @param publishJobResources the publish job resources 710 */ 711 public synchronized void addPublishJob(List<CmsPublishedResource> publishJobResources) { 712 713 boolean needToFlush = false; 714 Map<CmsUUID, List<CmsPublishedResource>> publishMap = new HashMap<>(); 715 for (CmsPublishedResource resource : publishJobResources) { 716 publishMap.computeIfAbsent(resource.getStructureId(), id -> new ArrayList<>()).add(resource); 717 } 718 for (CmsUUID id : publishMap.keySet()) { 719 if (isMove(m_currentBatch.get(id))) { 720 needToFlush = true; 721 } 722 } 723 if (needToFlush) { 724 if (m_flushTask != null) { 725 m_flushTask.cancel(false); 726 m_flushTask = null; 727 } 728 flush(); 729 } 730 m_currentBatch.putAll(publishMap); 731 if (m_flushTask == null) { 732 m_flushTask = m_flushExecutor.schedule( 733 this::flush, 734 CmsModificationContext.getOnlineFolderOptions().getIndexingInterval(), 735 TimeUnit.MILLISECONDS); 736 } 737 738 } 739 740 /** 741 * Checks if there is currently any work left to do for the instant publish indexing queue. 742 */ 743 public synchronized boolean hasWorkToDo() { 744 745 return (m_currentBatch.size() > 0) || (m_executor.getActiveCount() > 0) || !m_workQueue.isEmpty(); 746 } 747 748 /** 749 * Shuts down the queue. 750 */ 751 public void shutdown() { 752 753 // Tasks running in the flush executor produce tasks for the indexing executor, so we shut down the former before the latter to avoid skipping indexing during shutdown 754 m_flushExecutor.shutdown(); 755 try { 756 m_flushExecutor.awaitTermination(30, TimeUnit.SECONDS); 757 } catch (InterruptedException e) { 758 LOG.error(e.getLocalizedMessage(), e); 759 } 760 m_executor.shutdown(); 761 try { 762 m_executor.awaitTermination(30, TimeUnit.SECONDS); 763 } catch (InterruptedException e) { 764 LOG.error(e.getLocalizedMessage(), e); 765 } 766 } 767 768 /** 769 * Flushes the currently collected batch of published resources and submits them for indexing. 770 */ 771 protected synchronized void flush() { 772 773 m_flushTask = null; 774 775 List<CmsPublishedResource> resources = new ArrayList<>(); 776 for (List<CmsPublishedResource> entriesForId : m_currentBatch.values()) { 777 resources.addAll(entriesForId); 778 } 779 m_currentBatch.clear(); 780 if (resources.size() > 0) { 781 m_executor.submit(() -> tryIndex(resources)); 782 } 783 } 784 785 /** 786 * Indexes the given list of published resources. 787 * 788 * @param resourceList the resources to index 789 */ 790 protected void tryIndex(List<CmsPublishedResource> resourceList) { 791 792 try { 793 List<CmsPublishedResource> resourcesToIndex = computeUpdateResources(m_adminCms, resourceList); 794 long start = System.currentTimeMillis(); 795 ONLINE_LOCK.lock(false); 796 try { 797 updateAllIndexes(m_adminCms, resourcesToIndex, null); 798 } finally { 799 ONLINE_LOCK.unlock(); 800 long end = System.currentTimeMillis(); 801 LOG.info( 802 "Instant publish indexing of a batch of size " 803 + resourcesToIndex.size() 804 + " took " 805 + (end - start) 806 + "ms"); 807 } 808 } catch (Exception e) { 809 LOG.error(e.getLocalizedMessage(), e); 810 } 811 } 812 813 private boolean isMove(Collection<CmsPublishedResource> publishedResources) { 814 815 return (publishedResources != null) 816 && (publishedResources.size() == 2) 817 && publishedResources.stream().map(res -> res.getState()).collect(Collectors.toSet()).equals( 818 Set.of(CmsResource.STATE_DELETED, CmsResource.STATE_NEW)); 819 } 820 821 } 822 823 /** A constant for the optional 'baseUri' parameter. */ 824 public static final String PARAM_BASE_URI = "baseUri"; 825 826 /** A constant for the optional 'core' parameter. */ 827 public static final String PARAM_CORE = "core"; 828 829 /** A constant for the optional 'index' parameter. */ 830 public static final String PARAM_INDEX = "index"; 831 832 /** This needs to be a fair lock to preserve order of threads accessing the search manager. */ 833 private static final CmsPriorityLock OFFLINE_LOCK = new CmsPriorityLock(); 834 835 /** This needs to be a fair lock to preserve order of threads accessing the search manager. */ 836 private static final CmsPriorityLock ONLINE_LOCK = new CmsPriorityLock(); 837 838 /** The default value used for generating search result excerpts (1024 chars). */ 839 public static final int DEFAULT_EXCERPT_LENGTH = 1024; 840 841 /** The default value used for keeping the extraction results in the cache (672 hours = 4 weeks). */ 842 public static final float DEFAULT_EXTRACTION_CACHE_MAX_AGE = 672.0f; 843 844 /** Default for the maximum number of modifications before a commit in the search index is triggered (500). */ 845 public static final int DEFAULT_MAX_MODIFICATIONS_BEFORE_COMMIT = 500; 846 847 /** The default update frequency for offline indexes (15000 msec = 15 sec). */ 848 public static final int DEFAULT_OFFLINE_UPDATE_FREQNENCY = 15000; 849 850 /** The default maximal wait time for re-indexing after editing a content. */ 851 public static final int DEFAULT_MAX_INDEX_WAITTIME = 30000; 852 853 /** The default timeout value used for generating a document for the search index (60000 msec = 1 min). */ 854 public static final int DEFAULT_TIMEOUT = 60000; 855 856 /** Scheduler parameter: Update only a specified list of indexes. */ 857 public static final String JOB_PARAM_INDEXLIST = "indexList"; 858 859 /** Scheduler parameter: Write the output of the update to the logfile. */ 860 public static final String JOB_PARAM_WRITELOG = "writeLog"; 861 862 /** Prefix for Lucene default analyzers package (<code>org.apache.lucene.analysis.</code>). */ 863 public static final String LUCENE_ANALYZER = "org.apache.lucene.analysis.core."; 864 865 /** Secret key for the Solr basic auth information in form 'user:password' when read from the secret store. */ 866 protected static final String SECRET_KEY_SOLR_BASIC_AUTH_CREDENTIALS = "solr.basic.auth"; 867 868 /** The log object for this class. */ 869 protected static final Log LOG = CmsLog.getLog(CmsSearchManager.class); 870 871 /** List of resource types which represent groups of elements. */ 872 private static final String[] groupTypes = { 873 CmsResourceTypeXmlContainerPage.MODEL_GROUP_TYPE_NAME, 874 CmsResourceTypeXmlContainerPage.GROUP_CONTAINER_TYPE_NAME, 875 CmsResourceTypeXmlContainerPage.INHERIT_CONTAINER_TYPE_NAME}; 876 877 /** The indexing queue for the 'instant publish' feature. */ 878 private InstantPublishIndexingQueue m_instantPublishIndexQueue = new InstantPublishIndexingQueue(); 879 880 /** The administrator OpenCms user context to access OpenCms VFS resources. */ 881 protected CmsObject m_adminCms; 882 883 /** The list of indexes that are configured for offline index mode. */ 884 protected List<I_CmsSearchIndex> m_offlineIndexes; 885 886 /** The thread used of offline indexing. */ 887 protected CmsSearchOfflineIndexThread m_offlineIndexThread; 888 889 /** Configured analyzers for languages using <analyzer>. */ 890 private HashMap<Locale, CmsSearchAnalyzer> m_analyzers; 891 892 /** Stores the offline update frequency while indexing is paused. */ 893 private long m_configuredOfflineIndexingFrequency; 894 895 /** The Solr core container. */ 896 private CoreContainer m_coreContainer; 897 898 /** A map of document factory configurations. */ 899 private List<CmsSearchDocumentType> m_documentTypeConfigs; 900 901 /** A map of document factories keyed first by their name and then by their extraction keys. */ 902 private Map<String, Map<String, I_CmsDocumentFactory>> m_documentTypes; 903 904 /** The set of all globally available extraction keys for document factories. */ 905 private Set<String> m_extractionKeys; 906 907 /** The max age for extraction results to remain in the cache. */ 908 private float m_extractionCacheMaxAge; 909 910 /** The cache for the extraction results. */ 911 private CmsExtractionResultCache m_extractionResultCache; 912 913 /** Contains the available field configurations. */ 914 private Map<String, I_CmsSearchFieldConfiguration> m_fieldConfigurations; 915 916 /** The force unlock type. */ 917 private CmsSearchForceUnlockMode m_forceUnlockMode; 918 919 /** The class used to highlight the search terms in the excerpt of a search result. */ 920 private I_CmsTermHighlighter m_highlighter; 921 922 /** A list of search indexes. */ 923 private List<I_CmsSearchIndex> m_indexes; 924 925 /** Seconds to wait for an index lock. */ 926 private int m_indexLockMaxWaitSeconds = 10; 927 928 /** Configured index sources. */ 929 private Map<String, CmsSearchIndexSource> m_indexSources; 930 931 /** The max. char. length of the excerpt in the search result. */ 932 private int m_maxExcerptLength; 933 934 /** The maximum number of modifications before a commit in the search index is triggered. */ 935 private int m_maxModificationsBeforeCommit; 936 937 /** The offline index search handler. */ 938 private CmsSearchOfflineHandler m_offlineHandler; 939 940 /** The update frequency of the offline indexer in milliseconds. */ 941 private long m_offlineUpdateFrequency; 942 943 /** The maximal time to wait for re-indexing after a content is edited (in milliseconds). */ 944 private long m_maxIndexWaitTime; 945 946 /** Path to index files below WEB-INF/. */ 947 private String m_path; 948 949 /** The Solr configuration. */ 950 private CmsSolrConfiguration m_solrConfig; 951 952 /** Timeout for abandoning indexing thread. */ 953 private long m_timeout; 954 955 /** Offline indexing pause requests */ 956 private final Set<CmsUUID> m_pauseRequests = new HashSet<>(); 957 958 /** 959 * Default constructor when called as cron job.<p> 960 */ 961 public CmsSearchManager() { 962 963 m_documentTypes = new HashMap<String, Map<String, I_CmsDocumentFactory>>(); 964 m_extractionKeys = new HashSet<String>(); 965 m_documentTypeConfigs = new ArrayList<CmsSearchDocumentType>(); 966 m_analyzers = new HashMap<Locale, CmsSearchAnalyzer>(); 967 m_indexes = new ArrayList<I_CmsSearchIndex>(); 968 m_indexSources = new TreeMap<String, CmsSearchIndexSource>(); 969 m_offlineHandler = new CmsSearchOfflineHandler(); 970 m_extractionCacheMaxAge = DEFAULT_EXTRACTION_CACHE_MAX_AGE; 971 m_maxExcerptLength = DEFAULT_EXCERPT_LENGTH; 972 m_offlineUpdateFrequency = DEFAULT_OFFLINE_UPDATE_FREQNENCY; 973 m_maxIndexWaitTime = DEFAULT_MAX_INDEX_WAITTIME; 974 m_maxModificationsBeforeCommit = DEFAULT_MAX_MODIFICATIONS_BEFORE_COMMIT; 975 976 m_fieldConfigurations = new HashMap<String, I_CmsSearchFieldConfiguration>(); 977 // make sure we have a "standard" field configuration 978 addFieldConfiguration(CmsLuceneFieldConfiguration.DEFAULT_STANDARD); 979 980 if (CmsLog.INIT.isInfoEnabled()) { 981 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_START_SEARCH_CONFIG_0)); 982 } 983 } 984 985 /** 986 * Returns an analyzer for the given class name.<p> 987 * 988 * @param className the class name of the analyzer 989 * 990 * @return the appropriate lucene analyzer 991 * 992 * @throws Exception if something goes wrong 993 */ 994 public static Analyzer getAnalyzer(String className) throws Exception { 995 996 Analyzer analyzer = null; 997 Class<?> analyzerClass; 998 try { 999 analyzerClass = Class.forName(className); 1000 } catch (ClassNotFoundException e) { 1001 // allow Lucene standard classes to be written in a short form 1002 analyzerClass = Class.forName(LUCENE_ANALYZER + className); 1003 } 1004 1005 // since Lucene 3.0 most analyzers need a "version" parameter and don't support an empty constructor 1006 if (StandardAnalyzer.class.equals(analyzerClass)) { 1007 // the Lucene standard analyzer is used - but without any stopwords. 1008 analyzer = new StandardAnalyzer(new CharArraySet(0, false)); 1009 } else { 1010 analyzer = (Analyzer)analyzerClass.newInstance(); 1011 } 1012 return analyzer; 1013 } 1014 1015 /** 1016 * Returns the Solr index configured with the parameters name. 1017 * The parameters must contain a key/value pair with an existing 1018 * Solr index, otherwise <code>null</code> is returned.<p> 1019 * 1020 * @param cms the current context 1021 * @param params the parameter map 1022 * 1023 * @return the best matching Solr index 1024 */ 1025 public static final CmsSolrIndex getIndexSolr(CmsObject cms, Map<String, String[]> params) { 1026 1027 String indexName = null; 1028 CmsSolrIndex index = null; 1029 // try to get the index name from the parameters: 'core' or 'index' 1030 if (params != null) { 1031 indexName = params.get(PARAM_CORE) != null 1032 ? params.get(PARAM_CORE)[0] 1033 : (params.get(PARAM_INDEX) != null ? params.get(PARAM_INDEX)[0] : null); 1034 } 1035 if (indexName == null) { 1036 // if no parameter is specified try to use the default online/offline indexes by context 1037 indexName = cms.getRequestContext().getCurrentProject().isOnlineProject() 1038 ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE 1039 : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE; 1040 } 1041 // try to get the index 1042 index = OpenCms.getSearchManager().getIndexSolr(indexName); 1043 if (index == null) { 1044 // if there is exactly one index, a missing core / index parameter doesn't matter, since there is no choice. 1045 List<CmsSolrIndex> solrs = OpenCms.getSearchManager().getAllSolrIndexes(); 1046 if ((solrs != null) && !solrs.isEmpty() && (solrs.size() == 1)) { 1047 index = solrs.get(0); 1048 } 1049 } 1050 return index; 1051 } 1052 1053 /** 1054 * Returns <code>true</code> if the index for the given name is a Lucene index, <code>false</code> otherwise.<p> 1055 * 1056 * @param indexName the name of the index to check 1057 * 1058 * @return <code>true</code> if the index for the given name is a Lucene index 1059 */ 1060 public static boolean isLuceneIndex(String indexName) { 1061 1062 I_CmsSearchIndex i = OpenCms.getSearchManager().getIndex(indexName); 1063 return (i instanceof CmsSearchIndex) && (!(i instanceof CmsSolrIndex)); 1064 } 1065 1066 /** 1067 * Adds an analyzer.<p> 1068 * 1069 * @param analyzer an analyzer 1070 */ 1071 public void addAnalyzer(CmsSearchAnalyzer analyzer) { 1072 1073 m_analyzers.put(analyzer.getLocale(), analyzer); 1074 1075 if (CmsLog.INIT.isInfoEnabled()) { 1076 CmsLog.INIT.info( 1077 Messages.get().getBundle().key( 1078 Messages.INIT_ADD_ANALYZER_2, 1079 analyzer.getLocale(), 1080 analyzer.getClassName())); 1081 } 1082 } 1083 1084 /** 1085 * Adds a document type.<p> 1086 * 1087 * @param documentType a document type 1088 */ 1089 public void addDocumentTypeConfig(CmsSearchDocumentType documentType) { 1090 1091 m_documentTypeConfigs.add(documentType); 1092 1093 if (CmsLog.INIT.isInfoEnabled()) { 1094 CmsLog.INIT.info( 1095 Messages.get().getBundle().key( 1096 Messages.INIT_SEARCH_DOC_TYPES_2, 1097 documentType.getName(), 1098 documentType.getClassName())); 1099 } 1100 } 1101 1102 /** 1103 * Adds a search field configuration to the search manager.<p> 1104 * 1105 * @param fieldConfiguration the search field configuration to add 1106 */ 1107 public void addFieldConfiguration(I_CmsSearchFieldConfiguration fieldConfiguration) { 1108 1109 m_fieldConfigurations.put(fieldConfiguration.getName(), fieldConfiguration); 1110 } 1111 1112 /** 1113 * Adds a search index to the configuration.<p> 1114 * 1115 * @param searchIndex the search index to add 1116 */ 1117 public void addSearchIndex(I_CmsSearchIndex searchIndex) { 1118 1119 if (!searchIndex.isInitialized()) { 1120 if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) { 1121 try { 1122 searchIndex.initialize(); 1123 } catch (CmsException e) { 1124 // should never happen 1125 LOG.error(e.getMessage(), e); 1126 } 1127 } 1128 } 1129 1130 // name: not null or emtpy and unique 1131 String name = searchIndex.getName(); 1132 if (CmsStringUtil.isEmptyOrWhitespaceOnly(name)) { 1133 throw new CmsIllegalArgumentException( 1134 Messages.get().container(Messages.ERR_SEARCHINDEX_CREATE_MISSING_NAME_0)); 1135 } 1136 if (m_indexSources.keySet().contains(name)) { 1137 throw new CmsIllegalArgumentException( 1138 Messages.get().container(Messages.ERR_SEARCHINDEX_CREATE_INVALID_NAME_1, name)); 1139 } 1140 1141 m_indexes.add(searchIndex); 1142 if (m_adminCms != null) { 1143 initOfflineIndexes(); 1144 } 1145 1146 if (CmsLog.INIT.isInfoEnabled()) { 1147 CmsLog.INIT.info( 1148 Messages.get().getBundle().key( 1149 Messages.INIT_ADD_SEARCH_INDEX_2, 1150 searchIndex.getName(), 1151 searchIndex.getProject())); 1152 } 1153 } 1154 1155 /** 1156 * Adds a search index source configuration.<p> 1157 * 1158 * @param searchIndexSource a search index source configuration 1159 */ 1160 public void addSearchIndexSource(CmsSearchIndexSource searchIndexSource) { 1161 1162 m_indexSources.put(searchIndexSource.getName(), searchIndexSource); 1163 1164 if (CmsLog.INIT.isInfoEnabled()) { 1165 CmsLog.INIT.info( 1166 Messages.get().getBundle().key( 1167 Messages.INIT_SEARCH_INDEX_SOURCE_2, 1168 searchIndexSource.getName(), 1169 searchIndexSource.getIndexerClassName())); 1170 } 1171 } 1172 1173 /** 1174 * Implements the event listener of this class.<p> 1175 * 1176 * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) 1177 */ 1178 public void cmsEvent(CmsEvent event) { 1179 1180 switch (event.getType()) { 1181 case I_CmsEventListener.EVENT_REBUILD_SEARCHINDEXES: 1182 List<String> indexNames = null; 1183 if ((event.getData() != null) 1184 && CmsStringUtil.isNotEmptyOrWhitespaceOnly( 1185 (String)event.getData().get(I_CmsEventListener.KEY_INDEX_NAMES))) { 1186 indexNames = CmsStringUtil.splitAsList( 1187 (String)event.getData().get(I_CmsEventListener.KEY_INDEX_NAMES), 1188 ",", 1189 true); 1190 } 1191 try { 1192 if (LOG.isDebugEnabled()) { 1193 LOG.debug( 1194 Messages.get().getBundle().key( 1195 Messages.LOG_EVENT_REBUILD_SEARCHINDEX_1, 1196 indexNames == null ? "" : CmsStringUtil.collectionAsString(indexNames, ",")), 1197 new Exception()); 1198 } 1199 if (indexNames == null) { 1200 rebuildAllIndexes(getEventReport(event)); 1201 } else { 1202 rebuildIndexes(indexNames, getEventReport(event)); 1203 } 1204 } catch (CmsException e) { 1205 if (LOG.isErrorEnabled()) { 1206 LOG.error( 1207 Messages.get().getBundle().key( 1208 Messages.ERR_EVENT_REBUILD_SEARCHINDEX_1, 1209 indexNames == null ? "" : CmsStringUtil.collectionAsString(indexNames, ",")), 1210 e); 1211 } 1212 } 1213 break; 1214 case I_CmsEventListener.EVENT_CLEAR_CACHES: 1215 if (LOG.isDebugEnabled()) { 1216 LOG.debug(Messages.get().getBundle().key(Messages.LOG_EVENT_CLEAR_CACHES_0), new Exception()); 1217 } 1218 break; 1219 case I_CmsEventListener.EVENT_PUBLISH_PROJECT: 1220 // event data contains a list of the published resources 1221 CmsUUID publishHistoryId = new CmsUUID((String)event.getData().get(I_CmsEventListener.KEY_PUBLISHID)); 1222 if (LOG.isDebugEnabled()) { 1223 LOG.debug(Messages.get().getBundle().key(Messages.LOG_EVENT_PUBLISH_PROJECT_1, publishHistoryId)); 1224 } 1225 boolean instantPublish = Boolean.TRUE.equals( 1226 event.getData().get(I_CmsEventListener.KEY_INSTANT_PUBLISH)); 1227 if (instantPublish) { 1228 String publishIdStr = (String)event.getData().get(I_CmsEventListener.KEY_PUBLISHID); 1229 if (CmsUUID.isValidUUID(publishIdStr)) { 1230 CmsUUID publishId = new CmsUUID(publishIdStr); 1231 List<CmsPublishedResource> publishedResources; 1232 try { 1233 publishedResources = m_adminCms.readPublishedResources(publishId); 1234 m_instantPublishIndexQueue.addPublishJob(publishedResources); 1235 } catch (CmsException e) { 1236 LOG.error(e.getLocalizedMessage(), e); 1237 } 1238 } 1239 } else { 1240 updateAllIndexes(m_adminCms, publishHistoryId, getEventReport(event)); 1241 if (LOG.isDebugEnabled()) { 1242 LOG.debug( 1243 Messages.get().getBundle().key( 1244 Messages.LOG_EVENT_PUBLISH_PROJECT_FINISHED_1, 1245 publishHistoryId)); 1246 } 1247 } 1248 break; 1249 case I_CmsEventListener.EVENT_REINDEX_OFFLINE: 1250 case I_CmsEventListener.EVENT_REINDEX_ONLINE: 1251 boolean isOnline = I_CmsEventListener.EVENT_REINDEX_ONLINE == event.getType(); 1252 CmsPriorityLock lock = isOnline ? ONLINE_LOCK : OFFLINE_LOCK; 1253 Map<String, Object> eventData = event.getData(); 1254 CmsUUID userId = (CmsUUID)eventData.get(I_CmsEventListener.KEY_USER_ID); 1255 CmsUser user = null; 1256 if (userId != null) { 1257 try { 1258 user = m_adminCms.readUser(userId); 1259 } catch (Throwable t) { 1260 // should not normally happen 1261 LOG.debug(t.getMessage(), t); 1262 } 1263 } 1264 lock.lock(true); 1265 try { 1266 1267 if (LOG.isDebugEnabled()) { 1268 LOG.debug(Messages.get().getBundle().key(Messages.LOG_EVENT_REINDEX_STARTED_0)); 1269 } 1270 CmsObject cms = m_adminCms; 1271 if (!isOnline) { 1272 OpenCms.initCmsObject(m_adminCms); 1273 cms.getRequestContext().setCurrentProject( 1274 cms.readProject((CmsUUID)eventData.get(I_CmsEventListener.KEY_PROJECTID))); 1275 } 1276 @SuppressWarnings("unchecked") 1277 List<CmsResource> resources = (List<CmsResource>)eventData.get(I_CmsEventListener.KEY_RESOURCES); 1278 I_CmsReport report = (I_CmsReport)eventData.get(I_CmsEventListener.KEY_REPORT); 1279 List<CmsResource> resourcesToIndex = new ArrayList<>(); 1280 for (CmsResource res : resources) { 1281 if (res.isFile()) { 1282 resourcesToIndex.add(res); 1283 } else { 1284 try { 1285 resourcesToIndex.addAll( 1286 cms.readResources(res, CmsResourceFilter.IGNORE_EXPIRATION, true)); 1287 } catch (CmsException e) { 1288 LOG.error(e, e); 1289 } 1290 } 1291 } 1292 // we reindex and prevent using cached results 1293 cleanExtractionCache(); 1294 List<CmsPublishedResource> publishedResourcesToIndex = resourcesToIndex.stream().map( 1295 res -> new CmsPublishedResource(res)).collect(Collectors.toList()); 1296 if (Boolean.TRUE.equals(eventData.get(I_CmsEventListener.KEY_REINDEX_RELATED))) { 1297 addAdditionallyAffectedResources(cms, publishedResourcesToIndex); 1298 } 1299 if (isOnline) { 1300 updateAllIndexes( 1301 m_adminCms, 1302 publishedResourcesToIndex, 1303 new CmsShellLogReport(CmsLocaleManager.MASTER_LOCALE)); 1304 } else { 1305 updateIndexOffline(report, publishedResourcesToIndex); 1306 } 1307 cms = null; 1308 if (null != user) { 1309 Locale l = OpenCms.getWorkplaceManager().getWorkplaceLocale(user); 1310 OpenCms.getSessionManager().sendBroadcast( 1311 null, 1312 Messages.get().getBundle(l).key(Messages.GUI_REINDEXING_SUCCESS_0), 1313 user, 1314 ContentMode.html); 1315 } 1316 if (LOG.isDebugEnabled()) { 1317 LOG.debug(Messages.get().getBundle().key(Messages.LOG_EVENT_REINDEX_FINISHED_0)); 1318 } 1319 1320 } catch (Throwable e) { 1321 if (null != user) { 1322 Locale l = OpenCms.getWorkplaceManager().getWorkplaceLocale(user); 1323 OpenCms.getSessionManager().sendBroadcast( 1324 null, 1325 Messages.get().getBundle(l).key(Messages.GUI_REINDEXING_FAILED_0), 1326 user, 1327 ContentMode.html); 1328 } 1329 if (LOG.isDebugEnabled()) { 1330 LOG.error( 1331 Messages.get().getBundle().key(Messages.ERR_EVENT_REINDEX_FAILED_1, event.getData()), 1332 e); 1333 } else if (LOG.isErrorEnabled()) { 1334 LOG.error(Messages.get().getBundle().key(Messages.ERR_EVENT_REINDEX_FAILED_1, event.getData())); 1335 } 1336 } finally { 1337 lock.unlock(); 1338 } 1339 break; 1340 default: 1341 // no operation 1342 } 1343 } 1344 1345 /** 1346 * Returns all Solr index.<p> 1347 * 1348 * @return all Solr indexes 1349 */ 1350 public List<CmsSolrIndex> getAllSolrIndexes() { 1351 1352 List<CmsSolrIndex> result = new ArrayList<CmsSolrIndex>(); 1353 for (String indexName : getIndexNames()) { 1354 CmsSolrIndex index = getIndexSolr(indexName); 1355 if (index != null) { 1356 result.add(index); 1357 } 1358 } 1359 return result; 1360 } 1361 1362 /** 1363 * Returns an analyzer for the given language.<p> 1364 * 1365 * The analyzer is selected according to the analyzer configuration.<p> 1366 * 1367 * @param locale the locale to get the analyzer for 1368 * @return the appropriate lucene analyzer 1369 * 1370 * @throws CmsSearchException if something goes wrong 1371 */ 1372 public Analyzer getAnalyzer(Locale locale) throws CmsSearchException { 1373 1374 Analyzer analyzer = null; 1375 String className = null; 1376 1377 CmsSearchAnalyzer analyzerConf = m_analyzers.get(locale); 1378 if (analyzerConf == null) { 1379 throw new CmsSearchException(Messages.get().container(Messages.ERR_ANALYZER_NOT_FOUND_1, locale)); 1380 } 1381 1382 try { 1383 analyzer = getAnalyzer(analyzerConf.getClassName()); 1384 } catch (Exception e) { 1385 throw new CmsSearchException(Messages.get().container(Messages.ERR_LOAD_ANALYZER_1, className), e); 1386 } 1387 1388 return analyzer; 1389 } 1390 1391 /** 1392 * Returns an unmodifiable view of the map that contains the {@link CmsSearchAnalyzer} list.<p> 1393 * 1394 * The keys in the map are {@link Locale} objects, and the values are {@link CmsSearchAnalyzer} objects. 1395 * 1396 * @return an unmodifiable view of the Analyzers Map 1397 */ 1398 public Map<Locale, CmsSearchAnalyzer> getAnalyzers() { 1399 1400 return Collections.unmodifiableMap(m_analyzers); 1401 } 1402 1403 /** 1404 * Returns the search analyzer for the given locale.<p> 1405 * 1406 * @param locale the locale to get the analyzer for 1407 * 1408 * @return the search analyzer for the given locale 1409 */ 1410 public CmsSearchAnalyzer getCmsSearchAnalyzer(Locale locale) { 1411 1412 return m_analyzers.get(locale); 1413 } 1414 1415 /** 1416 * Returns the name of the directory below WEB-INF/ where the search indexes are stored.<p> 1417 * 1418 * @return the name of the directory below WEB-INF/ where the search indexes are stored 1419 */ 1420 public String getDirectory() { 1421 1422 return m_path; 1423 } 1424 1425 /** 1426 * Returns the configured Solr home directory <code>null</code> if not set.<p> 1427 * 1428 * @return the Solr home directory 1429 */ 1430 public String getDirectorySolr() { 1431 1432 return m_solrConfig != null ? m_solrConfig.getHome() : null; 1433 } 1434 1435 /** 1436 * Returns the document factory configured under the provided name. 1437 * @param docTypeName the name of the document type. 1438 * @return the factory for the provided name. 1439 */ 1440 public I_CmsDocumentFactory getDocumentFactoryForName(String docTypeName) { 1441 1442 Map<String, I_CmsDocumentFactory> factoryMap = m_documentTypes.get(docTypeName); 1443 if (factoryMap != null) { 1444 Iterator<I_CmsDocumentFactory> factoryIt = factoryMap.values().iterator(); 1445 if (factoryIt.hasNext()) { 1446 return factoryMap.values().iterator().next(); 1447 } 1448 } 1449 return null; 1450 } 1451 1452 /** 1453 * Returns a document type config.<p> 1454 * 1455 * @param name the name of the document type config 1456 * @return the document type config. 1457 */ 1458 public CmsSearchDocumentType getDocumentTypeConfig(String name) { 1459 1460 // this is really used only for the search manager GUI, 1461 // so performance is not an issue and no lookup map is generated 1462 for (int i = 0; i < m_documentTypeConfigs.size(); i++) { 1463 CmsSearchDocumentType type = m_documentTypeConfigs.get(i); 1464 if (type.getName().equals(name)) { 1465 return type; 1466 } 1467 } 1468 return null; 1469 } 1470 1471 /** 1472 * Returns an unmodifiable view (read-only) of the DocumentTypeConfigs Map.<p> 1473 * 1474 * @return an unmodifiable view (read-only) of the DocumentTypeConfigs Map 1475 */ 1476 public List<CmsSearchDocumentType> getDocumentTypeConfigs() { 1477 1478 return Collections.unmodifiableList(m_documentTypeConfigs); 1479 } 1480 1481 /** 1482 * Returns the document type keys used to specify the correct document factory. 1483 * 1484 * @see #getDocumentTypeKeys(String, String) for detailed information on the returned keys. 1485 * 1486 * @param resource the resource to generate the list of document type keys for. 1487 * @return the document type keys. 1488 */ 1489 public List<String> getDocumentTypeKeys(CmsResource resource) { 1490 1491 // first get the MIME type of the resource 1492 String mimeType = OpenCms.getResourceManager().getMimeType(resource.getRootPath(), null, "unknown"); 1493 String resourceType = null; 1494 try { 1495 resourceType = OpenCms.getResourceManager().getResourceType(resource.getTypeId()).getTypeName(); 1496 } catch (CmsLoaderException e) { 1497 // ignore, unknown resource type, resource can not be indexed 1498 LOG.info(e.getLocalizedMessage(), e); 1499 } 1500 return getDocumentTypeKeys(resourceType, mimeType); 1501 } 1502 1503 /** 1504 * Returns the document type keys used to specify the correct document factory. 1505 * One resource typically has more than one key. The document factories are matched 1506 * in the provided order and the first matching factory is used. 1507 * 1508 * The keys for type name "typename" and mimetype "mimetype" would be a subset of: 1509 * <ul> 1510 * <li><code>typename_mimetype</code></li> 1511 * <li><code>typename</code></li> 1512 * <li>if <code>typename</code> is a sub-type of <code>containerpage</code> 1513 * <ul> 1514 * <li><code>containerpage_mimetype</code></li> 1515 * <li><code>containerpage</code></li> 1516 * </ul> 1517 * </li> 1518 * <li>if <code>typename</code> is a sub-type of <code>xmlcontent</code> 1519 * <ul> 1520 * <li><code>xmlcontent_mimetype</code></li> 1521 * <li><code>xmlcontent</code></li> 1522 * </ul> 1523 * </li> 1524 * <li><code>__unconfigured___mimetype</code></li> 1525 * <li><code>__unconfigured__</code></li> 1526 * <li><code>__all___mimetype</code></li> 1527 * <li><code>__all__</code></li> 1528 * <ul> 1529 * Note that all keys except the "__all__"-keys are only added as long as globally 1530 * there is no matching factory for the key. 1531 * This in particular means that a factory matching "typename" will never be used 1532 * if you have a factory for "typename__mimetype" - even if this is not configured 1533 * for the used index source. Eventually, the content will not be indexed in such cases. 1534 * @param resourceType the resource type to generate the list of document type keys for. 1535 * @param mimeType the mime type to generate the list of document type keys for. 1536 * @return the document type keys. 1537 */ 1538 public List<String> getDocumentTypeKeys(String resourceType, String mimeType) { 1539 1540 List<String> result = new ArrayList<>(8); 1541 if (null != resourceType) { 1542 String currentKey = A_CmsVfsDocument.getDocumentKey(resourceType, mimeType); 1543 result.add(currentKey); 1544 if (!m_extractionKeys.contains(currentKey)) { 1545 currentKey = A_CmsVfsDocument.getDocumentKey(resourceType, null); 1546 result.add(currentKey); 1547 if (!m_extractionKeys.contains(currentKey)) { 1548 boolean hasGlobalMatch = false; 1549 try { 1550 String containerpageTypeName = CmsResourceTypeXmlContainerPage.getStaticTypeName(); 1551 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(resourceType); 1552 if (!resourceType.equals(containerpageTypeName)) { 1553 if (type instanceof CmsResourceTypeXmlContainerPage) { 1554 if (!resourceType.equals(CmsResourceTypeXmlContainerPage.getStaticTypeName())) { 1555 currentKey = A_CmsVfsDocument.getDocumentKey(containerpageTypeName, mimeType); 1556 result.add(currentKey); 1557 hasGlobalMatch = m_extractionKeys.contains(currentKey); 1558 if (!hasGlobalMatch) { 1559 currentKey = A_CmsVfsDocument.getDocumentKey(containerpageTypeName, null); 1560 result.add(currentKey); 1561 hasGlobalMatch = m_extractionKeys.contains(currentKey); 1562 } 1563 } 1564 } 1565 } 1566 String xmlcontentTypeName = CmsResourceTypeXmlContent.getStaticTypeName(); 1567 if (!resourceType.equals(containerpageTypeName)) { 1568 if (!hasGlobalMatch && (type instanceof CmsResourceTypeXmlContent)) { 1569 currentKey = A_CmsVfsDocument.getDocumentKey(xmlcontentTypeName, mimeType); 1570 result.add(currentKey); 1571 hasGlobalMatch = m_extractionKeys.contains(currentKey); 1572 if (!hasGlobalMatch) { 1573 currentKey = A_CmsVfsDocument.getDocumentKey(xmlcontentTypeName, null); 1574 result.add(currentKey); 1575 hasGlobalMatch = m_extractionKeys.contains(currentKey); 1576 } 1577 } 1578 } 1579 } catch (Throwable t) { 1580 LOG.warn("Could not read type for name \"" + resourceType + "\".", t); 1581 } 1582 if (!hasGlobalMatch) { 1583 result.add( 1584 A_CmsVfsDocument.getDocumentKey(A_CmsVfsDocument.DEFAULT_ALL_UNCONFIGURED_TYPES, mimeType)); 1585 result.add( 1586 A_CmsVfsDocument.getDocumentKey(A_CmsVfsDocument.DEFAULT_ALL_UNCONFIGURED_TYPES, null)); 1587 } 1588 } 1589 } 1590 result.add(A_CmsVfsDocument.getDocumentKey(A_CmsVfsDocument.DEFAULT_ALL_TYPES, mimeType)); 1591 result.add(A_CmsVfsDocument.getDocumentKey(A_CmsVfsDocument.DEFAULT_ALL_TYPES, null)); 1592 } 1593 return result; 1594 1595 } 1596 1597 /** 1598 * Returns the map from document type keys to document factories with all entries for the provided document type names. 1599 * @param documentTypeNames list of document type names to generate the map for. 1600 * @return the map from document type keys to document factories. 1601 */ 1602 public Map<String, I_CmsDocumentFactory> getDocumentTypeMapForTypeNames(List<String> documentTypeNames) { 1603 1604 Map<String, I_CmsDocumentFactory> result = new LinkedHashMap<>(); 1605 if (null != documentTypeNames) { 1606 // Iterate the list in reverse order to prefer factories that are added by document types listed earlier. 1607 ListIterator<String> typesIterator = documentTypeNames.listIterator(documentTypeNames.size()); 1608 while (typesIterator.hasPrevious()) { 1609 Map<String, I_CmsDocumentFactory> factories = m_documentTypes.get(typesIterator.previous()); 1610 if (null != factories) { 1611 result.putAll(factories); 1612 } 1613 } 1614 } 1615 return result; 1616 } 1617 1618 /** 1619 * Returns the maximum age a text extraction result is kept in the cache (in hours).<p> 1620 * 1621 * @return the maximum age a text extraction result is kept in the cache (in hours) 1622 */ 1623 public float getExtractionCacheMaxAge() { 1624 1625 return m_extractionCacheMaxAge; 1626 } 1627 1628 /** 1629 * Returns the search field configuration with the given name.<p> 1630 * 1631 * In case no configuration is available with the given name, <code>null</code> is returned.<p> 1632 * 1633 * @param name the name to get the search field configuration for 1634 * 1635 * @return the search field configuration with the given name 1636 */ 1637 public I_CmsSearchFieldConfiguration getFieldConfiguration(String name) { 1638 1639 return m_fieldConfigurations.get(name); 1640 } 1641 1642 /** 1643 * Returns the unmodifieable List of configured {@link I_CmsSearchFieldConfiguration} entries.<p> 1644 * 1645 * @return the unmodifieable List of configured {@link I_CmsSearchFieldConfiguration} entries 1646 */ 1647 public List<I_CmsSearchFieldConfiguration> getFieldConfigurations() { 1648 1649 List<I_CmsSearchFieldConfiguration> result = new ArrayList<I_CmsSearchFieldConfiguration>( 1650 m_fieldConfigurations.values()); 1651 Collections.sort(result); 1652 return Collections.unmodifiableList(result); 1653 } 1654 1655 /** 1656 * Returns the Lucene search field configurations only.<p> 1657 * 1658 * @return the Lucene search field configurations 1659 */ 1660 public List<CmsLuceneFieldConfiguration> getFieldConfigurationsLucene() { 1661 1662 List<CmsLuceneFieldConfiguration> result = new ArrayList<CmsLuceneFieldConfiguration>(); 1663 for (I_CmsSearchFieldConfiguration conf : m_fieldConfigurations.values()) { 1664 if (conf instanceof CmsLuceneFieldConfiguration) { 1665 result.add((CmsLuceneFieldConfiguration)conf); 1666 } 1667 } 1668 Collections.sort(result); 1669 return Collections.unmodifiableList(result); 1670 } 1671 1672 /** 1673 * Returns the Solr search field configurations only.<p> 1674 * 1675 * @return the Solr search field configurations 1676 */ 1677 public List<CmsSolrFieldConfiguration> getFieldConfigurationsSolr() { 1678 1679 List<CmsSolrFieldConfiguration> result = new ArrayList<CmsSolrFieldConfiguration>(); 1680 for (I_CmsSearchFieldConfiguration conf : m_fieldConfigurations.values()) { 1681 if (conf instanceof CmsSolrFieldConfiguration) { 1682 result.add((CmsSolrFieldConfiguration)conf); 1683 } 1684 } 1685 Collections.sort(result); 1686 return Collections.unmodifiableList(result); 1687 } 1688 1689 /** 1690 * Returns the force unlock mode during indexing.<p> 1691 * 1692 * @return the force unlock mode during indexing 1693 */ 1694 public CmsSearchForceUnlockMode getForceunlock() { 1695 1696 return m_forceUnlockMode; 1697 } 1698 1699 /** 1700 * Returns the highlighter.<p> 1701 * 1702 * @return the highlighter 1703 */ 1704 public I_CmsTermHighlighter getHighlighter() { 1705 1706 return m_highlighter; 1707 } 1708 1709 /** 1710 * Returns the Lucene search index configured with the given name.<p> 1711 * The index must exist, otherwise <code>null</code> is returned. 1712 * 1713 * @param indexName then name of the requested search index 1714 * 1715 * @return the Lucene search index configured with the given name 1716 */ 1717 public I_CmsSearchIndex getIndex(String indexName) { 1718 1719 for (I_CmsSearchIndex index : m_indexes) { 1720 if (indexName.equalsIgnoreCase(index.getName())) { 1721 return index; 1722 } 1723 } 1724 return null; 1725 } 1726 1727 /** 1728 * Returns the seconds to wait for an index lock during an update operation.<p> 1729 * 1730 * @return the seconds to wait for an index lock during an update operation 1731 */ 1732 public int getIndexLockMaxWaitSeconds() { 1733 1734 return m_indexLockMaxWaitSeconds; 1735 } 1736 1737 /** 1738 * Returns the names of all configured indexes.<p> 1739 * 1740 * @return list of names 1741 */ 1742 public List<String> getIndexNames() { 1743 1744 List<String> indexNames = new ArrayList<String>(); 1745 for (int i = 0, n = m_indexes.size(); i < n; i++) { 1746 indexNames.add((m_indexes.get(i)).getName()); 1747 } 1748 1749 return indexNames; 1750 } 1751 1752 /** 1753 * Returns the Solr index configured with the given name.<p> 1754 * The index must exist, otherwise <code>null</code> is returned. 1755 * 1756 * @param indexName then name of the requested Solr index 1757 * @return the Solr index configured with the given name 1758 */ 1759 public CmsSolrIndex getIndexSolr(String indexName) { 1760 1761 I_CmsSearchIndex index = getIndex(indexName); 1762 if (index instanceof CmsSolrIndex) { 1763 return (CmsSolrIndex)index; 1764 } 1765 return null; 1766 } 1767 1768 /** 1769 * Returns a search index source for a specified source name.<p> 1770 * 1771 * @param sourceName the name of the index source 1772 * @return a search index source 1773 */ 1774 public CmsSearchIndexSource getIndexSource(String sourceName) { 1775 1776 return m_indexSources.get(sourceName); 1777 } 1778 1779 /** 1780 * Returns the max. excerpt length.<p> 1781 * 1782 * @return the max excerpt length 1783 */ 1784 public int getMaxExcerptLength() { 1785 1786 return m_maxExcerptLength; 1787 } 1788 1789 /** 1790 * Returns the maximal time to wait for re-indexing after a content is edited (in milliseconds).<p> 1791 * 1792 * @return the maximal time to wait for re-indexing after a content is edited (in milliseconds) 1793 */ 1794 public long getMaxIndexWaitTime() { 1795 1796 return m_maxIndexWaitTime; 1797 } 1798 1799 /** 1800 * Returns the maximum number of modifications before a commit in the search index is triggered.<p> 1801 * 1802 * @return the maximum number of modifications before a commit in the search index is triggered 1803 */ 1804 public int getMaxModificationsBeforeCommit() { 1805 1806 return m_maxModificationsBeforeCommit; 1807 } 1808 1809 /** 1810 * Returns the update frequency of the offline indexer in milliseconds.<p> 1811 * 1812 * @return the update frequency of the offline indexer in milliseconds 1813 */ 1814 public long getOfflineUpdateFrequency() { 1815 1816 return m_offlineUpdateFrequency; 1817 } 1818 1819 /** 1820 * Returns an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances.<p> 1821 * 1822 * @return an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances 1823 */ 1824 public List<I_CmsSearchIndex> getSearchIndexes() { 1825 1826 return Collections.unmodifiableList(m_indexes); 1827 } 1828 1829 /** 1830 * Returns an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances.<p> 1831 * 1832 * @return an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances 1833 */ 1834 public List<I_CmsSearchIndex> getSearchIndexesAll() { 1835 1836 return Collections.unmodifiableList(m_indexes); 1837 } 1838 1839 /** 1840 * Returns an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances.<p> 1841 * 1842 * @return an unmodifiable list of all configured <code>{@link I_CmsSearchIndex}</code> instances 1843 */ 1844 public List<CmsSolrIndex> getSearchIndexesSolr() { 1845 1846 List<CmsSolrIndex> indexes = new ArrayList<CmsSolrIndex>(); 1847 for (I_CmsSearchIndex index : m_indexes) { 1848 if (index instanceof CmsSolrIndex) { 1849 indexes.add((CmsSolrIndex)index); 1850 } 1851 } 1852 return Collections.unmodifiableList(indexes); 1853 } 1854 1855 /** 1856 * Returns an unmodifiable view (read-only) of the SearchIndexSources Map.<p> 1857 * 1858 * @return an unmodifiable view (read-only) of the SearchIndexSources Map 1859 */ 1860 public Map<String, CmsSearchIndexSource> getSearchIndexSources() { 1861 1862 return Collections.unmodifiableMap(m_indexSources); 1863 } 1864 1865 /** 1866 * Return singleton instance of the OpenCms spellchecker.<p> 1867 * 1868 * @return instance of CmsSolrSpellchecker. 1869 */ 1870 public CmsSolrSpellchecker getSolrDictionary() { 1871 1872 // get the core container that contains one core for each configured index 1873 if (m_coreContainer == null) { 1874 m_coreContainer = createCoreContainer(); 1875 } 1876 return CmsSolrSpellchecker.getInstance(m_coreContainer); 1877 } 1878 1879 /** 1880 * Returns the Solr configuration.<p> 1881 * 1882 * @return the Solr configuration 1883 */ 1884 public CmsSolrConfiguration getSolrServerConfiguration() { 1885 1886 return m_solrConfig; 1887 } 1888 1889 /** 1890 * Returns the timeout to abandon threads indexing a resource.<p> 1891 * 1892 * @return the timeout to abandon threads indexing a resource 1893 */ 1894 public long getTimeout() { 1895 1896 return m_timeout; 1897 } 1898 1899 /** 1900 * Initializes the search manager.<p> 1901 * 1902 * @param cms the cms object 1903 * 1904 * @throws CmsRoleViolationException in case the given opencms object does not have <code>{@link CmsRole#WORKPLACE_MANAGER}</code> permissions 1905 */ 1906 public void initialize(CmsObject cms) throws CmsRoleViolationException { 1907 1908 OpenCms.getRoleManager().checkRole(cms, CmsRole.WORKPLACE_MANAGER); 1909 try { 1910 // store the Admin cms to index Cms resources 1911 m_adminCms = OpenCms.initCmsObject(cms); 1912 } catch (CmsException e) { 1913 // this should never happen 1914 LOG.error(e.getLocalizedMessage(), e); 1915 } 1916 // make sure the site root is the root site 1917 m_adminCms.getRequestContext().setSiteRoot("/"); 1918 1919 // create the extraction result cache 1920 m_extractionResultCache = new CmsExtractionResultCache( 1921 OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf(getDirectory()), 1922 "/extractCache"); 1923 initializeFieldConfigurations(); 1924 initializeIndexes(); 1925 initOfflineIndexes(); 1926 1927 // register this object as event listener 1928 OpenCms.addCmsEventListener( 1929 this, 1930 new int[] { 1931 I_CmsEventListener.EVENT_CLEAR_CACHES, 1932 I_CmsEventListener.EVENT_PUBLISH_PROJECT, 1933 I_CmsEventListener.EVENT_REBUILD_SEARCHINDEXES, 1934 I_CmsEventListener.EVENT_REINDEX_OFFLINE, 1935 I_CmsEventListener.EVENT_REINDEX_ONLINE}); 1936 } 1937 1938 /** 1939 * Calls {@link I_CmsSearchFieldConfiguration#init()} for all registered field configurations. 1940 */ 1941 public void initializeFieldConfigurations() { 1942 1943 for (I_CmsSearchFieldConfiguration config : m_fieldConfigurations.values()) { 1944 config.init(); 1945 } 1946 1947 } 1948 1949 /** 1950 * Initializes all configured document types, index sources and search indexes.<p> 1951 * 1952 * This method needs to be called if after a change in the index configuration has been made. 1953 */ 1954 public void initializeIndexes() { 1955 1956 initAvailableDocumentTypes(); 1957 initIndexSources(); 1958 initSearchIndexes(); 1959 } 1960 1961 /** 1962 * Initialize the offline index handler, require after an offline index has been added.<p> 1963 */ 1964 public void initOfflineIndexes() { 1965 1966 // check which indexes are configured as offline indexes 1967 List<I_CmsSearchIndex> offlineIndexes = new ArrayList<I_CmsSearchIndex>(); 1968 Iterator<I_CmsSearchIndex> i = m_indexes.iterator(); 1969 while (i.hasNext()) { 1970 I_CmsSearchIndex index = i.next(); 1971 if (I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode())) { 1972 // this is an offline index 1973 offlineIndexes.add(index); 1974 } 1975 } 1976 m_offlineIndexes = offlineIndexes; 1977 m_offlineHandler.initialize(); 1978 1979 } 1980 1981 /** 1982 * Initializes the spell check index.<p> 1983 * 1984 * @param adminCms the ROOT_ADMIN cms context 1985 */ 1986 public void initSpellcheckIndex(CmsObject adminCms) { 1987 1988 if (CmsSpellcheckDictionaryIndexer.updatingIndexNecessesary(adminCms)) { 1989 final CmsSolrSpellchecker spellchecker = OpenCms.getSearchManager().getSolrDictionary(); 1990 if (spellchecker != null) { 1991 1992 Runnable initRunner = new Runnable() { 1993 1994 public void run() { 1995 1996 try { 1997 spellchecker.parseAndAddDictionaries(adminCms); 1998 } catch (CmsRoleViolationException e) { 1999 LOG.error(e.getLocalizedMessage(), e); 2000 } 2001 } 2002 }; 2003 new Thread(initRunner).start(); 2004 } 2005 } 2006 } 2007 2008 /** 2009 * Returns if the offline indexing is paused.<p> 2010 * 2011 * @return <code>true</code> if the offline indexing is paused 2012 */ 2013 public boolean isOfflineIndexingPaused() { 2014 2015 return m_offlineUpdateFrequency == Long.MAX_VALUE; 2016 } 2017 2018 /** 2019 * Updates the indexes from as a scheduled job.<p> 2020 * 2021 * @param cms the OpenCms user context to use when reading resources from the VFS 2022 * @param parameters the parameters for the scheduled job 2023 * 2024 * @throws Exception if something goes wrong 2025 * 2026 * @return the String to write in the scheduler log 2027 * 2028 * @see org.opencms.scheduler.I_CmsScheduledJob#launch(CmsObject, Map) 2029 */ 2030 public String launch(CmsObject cms, Map<String, String> parameters) throws Exception { 2031 2032 CmsSearchManager manager = OpenCms.getSearchManager(); 2033 2034 I_CmsReport report = null; 2035 boolean writeLog = Boolean.valueOf(parameters.get(JOB_PARAM_WRITELOG)).booleanValue(); 2036 2037 if (writeLog) { 2038 report = new CmsLogReport(cms.getRequestContext().getLocale(), CmsSearchManager.class); 2039 } 2040 2041 List<String> updateList = null; 2042 String indexList = parameters.get(JOB_PARAM_INDEXLIST); 2043 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(indexList)) { 2044 // index list has been provided as job parameter 2045 updateList = new ArrayList<String>(); 2046 String[] indexNames = CmsStringUtil.splitAsArray(indexList, '|'); 2047 for (int i = 0; i < indexNames.length; i++) { 2048 // check if the index actually exists 2049 if (manager.getIndex(indexNames[i]) != null) { 2050 updateList.add(indexNames[i]); 2051 } else { 2052 if (LOG.isWarnEnabled()) { 2053 LOG.warn(Messages.get().getBundle().key(Messages.LOG_NO_INDEX_WITH_NAME_1, indexNames[i])); 2054 } 2055 } 2056 } 2057 } 2058 2059 long startTime = System.currentTimeMillis(); 2060 2061 if (updateList == null) { 2062 // all indexes need to be updated 2063 manager.rebuildAllIndexes(report); 2064 } else { 2065 // rebuild only the selected indexes 2066 manager.rebuildIndexes(updateList, report); 2067 } 2068 2069 long runTime = System.currentTimeMillis() - startTime; 2070 2071 String finishMessage = Messages.get().getBundle().key( 2072 Messages.LOG_REBUILD_INDEXES_FINISHED_1, 2073 CmsStringUtil.formatRuntime(runTime)); 2074 2075 if (LOG.isInfoEnabled()) { 2076 LOG.info(finishMessage); 2077 } 2078 return finishMessage; 2079 } 2080 2081 /** 2082 * Pauses the offline indexing and returns a pause request id that has to be used for resuming offline indexing again.<p> 2083 * May take some time, because the indexes are updated first.<p> 2084 * 2085 *@return the pause request id. The id has to be given to the {@link #resumeOfflineIndexing(CmsUUID)} method to resume offline indexing. 2086 */ 2087 public CmsUUID pauseOfflineIndexing() { 2088 2089 CmsUUID pauseId = new CmsUUID(); 2090 synchronized (m_pauseRequests) { 2091 if (m_pauseRequests.isEmpty()) { 2092 LOG.info("Pausing offline indexing."); 2093 m_configuredOfflineIndexingFrequency = m_offlineUpdateFrequency; 2094 m_offlineUpdateFrequency = Long.MAX_VALUE; 2095 updateOfflineIndexes(0); 2096 } 2097 m_pauseRequests.add(pauseId); 2098 if (LOG.isDebugEnabled()) { 2099 LOG.debug("Added pause request with id " + pauseId); 2100 } 2101 } 2102 return pauseId; 2103 } 2104 2105 /** 2106 * Rebuilds (if required creates) all configured indexes.<p> 2107 * 2108 * @param report the report object to write messages (or <code>null</code>) 2109 * 2110 * @throws CmsException if something goes wrong 2111 */ 2112 public void rebuildAllIndexes(I_CmsReport report) throws CmsException { 2113 2114 OFFLINE_LOCK.lock(true); 2115 try { 2116 ONLINE_LOCK.lock(true); 2117 try { 2118 2119 CmsMessageContainer container = null; 2120 for (int i = 0, n = m_indexes.size(); i < n; i++) { 2121 // iterate all configured search indexes 2122 I_CmsSearchIndex searchIndex = m_indexes.get(i); 2123 try { 2124 // update the index 2125 updateIndex(searchIndex, report, null); 2126 } catch (CmsException e) { 2127 container = new CmsMessageContainer( 2128 Messages.get(), 2129 Messages.ERR_INDEX_REBUILD_ALL_1, 2130 new Object[] {searchIndex.getName()}); 2131 LOG.error( 2132 Messages.get().getBundle().key(Messages.ERR_INDEX_REBUILD_ALL_1, searchIndex.getName()), 2133 e); 2134 } 2135 } 2136 // clean up the extraction result cache 2137 cleanExtractionCache(); 2138 if (container != null) { 2139 // throw stored exception 2140 throw new CmsSearchException(container); 2141 } 2142 } finally { 2143 ONLINE_LOCK.unlock(); 2144 } 2145 } finally { 2146 OFFLINE_LOCK.unlock(); 2147 } 2148 } 2149 2150 /** 2151 * Rebuilds (if required creates) the index with the given name.<p> 2152 * 2153 * @param indexName the name of the index to rebuild 2154 * @param report the report object to write messages (or <code>null</code>) 2155 * 2156 * @throws CmsException if something goes wrong 2157 */ 2158 public void rebuildIndex(String indexName, I_CmsReport report) throws CmsException { 2159 2160 I_CmsSearchIndex index = getIndex(indexName); 2161 CmsPriorityLock lock = I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode()) 2162 ? OFFLINE_LOCK 2163 : ONLINE_LOCK; 2164 lock.lock(true); 2165 try { 2166 // update the index 2167 updateIndex(index, report, null); 2168 // clean up the extraction result cache 2169 cleanExtractionCache(); 2170 } finally { 2171 lock.unlock(); 2172 2173 } 2174 } 2175 2176 /** 2177 * Rebuilds (if required creates) the List of indexes with the given name.<p> 2178 * 2179 * @param indexNames the names (String) of the index to rebuild 2180 * @param report the report object to write messages (or <code>null</code>) 2181 * 2182 * @throws CmsException if something goes wrong 2183 */ 2184 public void rebuildIndexes(List<String> indexNames, I_CmsReport report) throws CmsException { 2185 2186 Iterator<String> i = indexNames.iterator(); 2187 while (i.hasNext()) { 2188 String indexName = i.next(); 2189 // get the search index by name 2190 I_CmsSearchIndex index = getIndex(indexName); 2191 if (index != null) { 2192 CmsPriorityLock lock = I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode()) 2193 ? OFFLINE_LOCK 2194 : ONLINE_LOCK; 2195 try { 2196 lock.lock(true); 2197 updateIndex(index, report, null); 2198 } finally { 2199 lock.unlock(); 2200 2201 } 2202 } else { 2203 if (LOG.isWarnEnabled()) { 2204 LOG.warn(Messages.get().getBundle().key(Messages.LOG_NO_INDEX_WITH_NAME_1, indexName)); 2205 } 2206 } 2207 } 2208 // clean up the extraction result cache 2209 cleanExtractionCache(); 2210 } 2211 2212 /** 2213 * Registers a new Solr core for the given index.<p> 2214 * 2215 * @param index the index to register a new Solr core for 2216 * 2217 * @throws CmsConfigurationException if no Solr server is configured 2218 */ 2219 @SuppressWarnings("resource") 2220 public void registerSolrIndex(CmsSolrIndex index) throws CmsConfigurationException { 2221 2222 if ((m_solrConfig == null) || !m_solrConfig.isEnabled()) { 2223 // No solr server configured 2224 throw new CmsConfigurationException(Messages.get().container(Messages.ERR_SOLR_NOT_ENABLED_0)); 2225 } 2226 2227 if (index.getServerUrl() != null) { // Use the index-specific Solr-Server if present. 2228 SolrClient solrClient = getSolrClient(index.getServerUrl()); 2229 index.setSolrServer(solrClient); 2230 } else if (m_solrConfig.getServerUrl() != null) { // Use the globally configured external Solr-Server if present. 2231 // HTTP Server configured 2232 // TODO Implement multi core support for HTTP server 2233 // @see http://lucidworks.lucidimagination.com/display/solr/Configuring+solr.xml 2234 index.setSolrServer(getSolrClient(m_solrConfig.getServerUrl())); 2235 } else { // Default to the embedded Solr Server 2236 2237 // get the core container that contains one core for each configured index 2238 if (m_coreContainer == null) { 2239 m_coreContainer = createCoreContainer(); 2240 } 2241 2242 // unload the existing core if it exists to avoid problems with forced unlock. 2243 if (m_coreContainer.getAllCoreNames().contains(index.getCoreName())) { 2244 m_coreContainer.unload(index.getCoreName(), false, false, true); 2245 } 2246 // ensure that all locks on the index are gone 2247 ensureIndexIsUnlocked(index.getPath()); 2248 2249 // load the core to the container 2250 File dataDir = new File(index.getPath()); 2251 if (!dataDir.exists()) { 2252 dataDir.mkdirs(); 2253 if (CmsLog.INIT.isInfoEnabled()) { 2254 CmsLog.INIT.info( 2255 Messages.get().getBundle().key( 2256 Messages.INIT_SOLR_INDEX_DIR_CREATED_2, 2257 index.getName(), 2258 index.getPath())); 2259 } 2260 } 2261 File instanceDir = new File( 2262 m_solrConfig.getHome() + FileSystems.getDefault().getSeparator() + index.getName()); 2263 if (!instanceDir.exists()) { 2264 instanceDir.mkdirs(); 2265 if (CmsLog.INIT.isInfoEnabled()) { 2266 CmsLog.INIT.info( 2267 Messages.get().getBundle().key( 2268 Messages.INIT_SOLR_INDEX_DIR_CREATED_2, 2269 index.getName(), 2270 index.getPath())); 2271 } 2272 } 2273 2274 // create the core 2275 // TODO: suboptimal - forces always the same schema 2276 SolrCore core = null; 2277 try { 2278 // creation includes registration. 2279 // TODO: this was the old code: core = m_coreContainer.create(descriptor, false); 2280 Map<String, String> properties = new HashMap<String, String>(3); 2281 properties.put(CoreDescriptor.CORE_DATADIR, dataDir.getAbsolutePath()); 2282 properties.put(CoreDescriptor.CORE_CONFIGSET, "default"); 2283 core = m_coreContainer.create(index.getCoreName(), instanceDir.toPath(), properties, false); 2284 } catch (RuntimeException e) { 2285 // catch not only NPE, but also e.g. SolrException when the core cannot be created 2286 // (like IndexFormatTooOldException for an index written by an older Lucene version): 2287 // otherwise the exception propagates past the CmsConfigurationException handling in 2288 // CmsSolrIndex#initialize(), the index stays enabled without a Solr client and every 2289 // query later fails with a NullPointerException on the missing client 2290 if (core != null) { 2291 core.close(); 2292 } 2293 throw new CmsConfigurationException( 2294 Messages.get().container( 2295 Messages.ERR_SOLR_SERVER_NOT_CREATED_3, 2296 index.getName() + " (" + index.getCoreName() + ")", 2297 index.getPath(), 2298 m_solrConfig.getSolrConfigFile().getAbsolutePath()), 2299 e); 2300 } 2301 2302 if (index.isNoSolrServerSet()) { 2303 index.setSolrServer(new EmbeddedSolrServer(m_coreContainer, index.getCoreName())); 2304 } 2305 if (CmsLog.INIT.isInfoEnabled()) { 2306 CmsLog.INIT.info( 2307 Messages.get().getBundle().key( 2308 Messages.INIT_SOLR_SERVER_CREATED_1, 2309 index.getName() + " (" + index.getCoreName() + ")")); 2310 } 2311 } 2312 } 2313 2314 /** 2315 * Removes this field configuration from the OpenCms configuration (if it is not used any more).<p> 2316 * 2317 * @param fieldConfiguration the field configuration to remove from the configuration 2318 * 2319 * @return true if remove was successful, false if preconditions for removal are ok but the given 2320 * field configuration was unknown to the manager. 2321 * 2322 * @throws CmsIllegalStateException if the given field configuration is still used by at least one 2323 * <code>{@link I_CmsSearchIndex}</code>. 2324 * 2325 */ 2326 public boolean removeSearchFieldConfiguration(I_CmsSearchFieldConfiguration fieldConfiguration) 2327 throws CmsIllegalStateException { 2328 2329 // never remove the standard field configuration 2330 if (fieldConfiguration.getName().equals(CmsSearchFieldConfiguration.STR_STANDARD)) { 2331 throw new CmsIllegalStateException( 2332 Messages.get().container( 2333 Messages.ERR_INDEX_CONFIGURATION_DELETE_STANDARD_1, 2334 fieldConfiguration.getName())); 2335 } 2336 // validation if removal will be granted 2337 Iterator<I_CmsSearchIndex> itIndexes = m_indexes.iterator(); 2338 I_CmsSearchIndex idx; 2339 // the list for collecting indexes that use the given field configuration 2340 List<I_CmsSearchIndex> referrers = new ArrayList<I_CmsSearchIndex>(); 2341 I_CmsSearchFieldConfiguration refFieldConfig; 2342 while (itIndexes.hasNext()) { 2343 idx = itIndexes.next(); 2344 refFieldConfig = idx.getFieldConfiguration(); 2345 if (refFieldConfig.equals(fieldConfiguration)) { 2346 referrers.add(idx); 2347 } 2348 } 2349 if (referrers.size() > 0) { 2350 throw new CmsIllegalStateException( 2351 Messages.get().container( 2352 Messages.ERR_INDEX_CONFIGURATION_DELETE_2, 2353 fieldConfiguration.getName(), 2354 referrers.toString())); 2355 } 2356 2357 // remove operation (no exception) 2358 return m_fieldConfigurations.remove(fieldConfiguration.getName()) != null; 2359 2360 } 2361 2362 /** 2363 * Removes a search field from the field configuration.<p> 2364 * 2365 * @param fieldConfiguration the field configuration 2366 * @param field field to remove from the field configuration 2367 * 2368 * @return true if remove was successful, false if preconditions for removal are ok but the given 2369 * field was unknown. 2370 */ 2371 public boolean removeSearchFieldConfigurationField( 2372 I_CmsSearchFieldConfiguration fieldConfiguration, 2373 CmsSearchField field) { 2374 2375 if (LOG.isInfoEnabled()) { 2376 LOG.info( 2377 Messages.get().getBundle().key( 2378 Messages.LOG_REMOVE_FIELDCONFIGURATION_FIELD_INDEX_2, 2379 field.getName(), 2380 fieldConfiguration.getName())); 2381 } 2382 2383 return fieldConfiguration.getFields().remove(field); 2384 } 2385 2386 /** 2387 * Removes a search field mapping from the given field.<p> 2388 * 2389 * @param field the field 2390 * @param mapping mapping to remove from the field 2391 * 2392 * @return true if remove was successful, false if preconditions for removal are ok but the given 2393 * mapping was unknown. 2394 * 2395 * @throws CmsIllegalStateException if the given mapping is the last mapping inside the given field. 2396 */ 2397 public boolean removeSearchFieldMapping(CmsLuceneField field, CmsSearchFieldMapping mapping) 2398 throws CmsIllegalStateException { 2399 2400 if (field.getMappings().size() < 2) { 2401 throw new CmsIllegalStateException( 2402 Messages.get().container( 2403 Messages.ERR_FIELD_MAPPING_DELETE_2, 2404 mapping.getType().toString(), 2405 field.getName())); 2406 } else { 2407 2408 if (LOG.isInfoEnabled()) { 2409 LOG.info( 2410 Messages.get().getBundle().key( 2411 Messages.LOG_REMOVE_FIELD_MAPPING_INDEX_2, 2412 mapping.toString(), 2413 field.getName())); 2414 } 2415 return field.getMappings().remove(mapping); 2416 } 2417 } 2418 2419 /** 2420 * Removes a search index from the configuration.<p> 2421 * 2422 * @param searchIndex the search index to remove 2423 */ 2424 public void removeSearchIndex(I_CmsSearchIndex searchIndex) { 2425 2426 // shut down index to remove potential config files of Solr indexes 2427 searchIndex.shutDown(); 2428 if (searchIndex instanceof CmsSolrIndex) { 2429 CmsSolrIndex solrIndex = (CmsSolrIndex)searchIndex; 2430 m_coreContainer.unload(solrIndex.getCoreName(), true, true, true); 2431 } 2432 m_indexes.remove(searchIndex); 2433 initOfflineIndexes(); 2434 2435 if (LOG.isInfoEnabled()) { 2436 LOG.info( 2437 Messages.get().getBundle().key( 2438 Messages.LOG_REMOVE_SEARCH_INDEX_2, 2439 searchIndex.getName(), 2440 searchIndex.getProject())); 2441 } 2442 } 2443 2444 /** 2445 * Removes all indexes included in the given list (which must contain the name of an index to remove).<p> 2446 * 2447 * @param indexNames the names of the index to remove 2448 */ 2449 public void removeSearchIndexes(List<String> indexNames) { 2450 2451 Iterator<String> i = indexNames.iterator(); 2452 while (i.hasNext()) { 2453 String indexName = i.next(); 2454 // get the search index by name 2455 I_CmsSearchIndex index = getIndex(indexName); 2456 if (index != null) { 2457 // remove the index 2458 removeSearchIndex(index); 2459 } else { 2460 if (LOG.isWarnEnabled()) { 2461 LOG.warn(Messages.get().getBundle().key(Messages.LOG_NO_INDEX_WITH_NAME_1, indexName)); 2462 } 2463 } 2464 } 2465 } 2466 2467 /** 2468 * Removes this indexsource from the OpenCms configuration (if it is not used any more).<p> 2469 * 2470 * @param indexsource the indexsource to remove from the configuration 2471 * 2472 * @return true if remove was successful, false if preconditions for removal are ok but the given 2473 * searchindex was unknown to the manager. 2474 * 2475 * @throws CmsIllegalStateException if the given indexsource is still used by at least one 2476 * <code>{@link I_CmsSearchIndex}</code>. 2477 * 2478 */ 2479 public boolean removeSearchIndexSource(CmsSearchIndexSource indexsource) throws CmsIllegalStateException { 2480 2481 // validation if removal will be granted 2482 Iterator<I_CmsSearchIndex> itIndexes = m_indexes.iterator(); 2483 I_CmsSearchIndex idx; 2484 // the list for collecting indexes that use the given index source 2485 List<I_CmsSearchIndex> referrers = new ArrayList<I_CmsSearchIndex>(); 2486 // the current list of referred index sources of the iterated index 2487 List<CmsSearchIndexSource> refsources; 2488 while (itIndexes.hasNext()) { 2489 idx = itIndexes.next(); 2490 refsources = idx.getSources(); 2491 if (refsources != null) { 2492 if (refsources.contains(indexsource)) { 2493 referrers.add(idx); 2494 } 2495 } 2496 } 2497 if (referrers.size() > 0) { 2498 throw new CmsIllegalStateException( 2499 Messages.get().container( 2500 Messages.ERR_INDEX_SOURCE_DELETE_2, 2501 indexsource.getName(), 2502 referrers.toString())); 2503 } 2504 2505 // remove operation (no exception) 2506 return m_indexSources.remove(indexsource.getName()) != null; 2507 2508 } 2509 2510 /** 2511 * Resumes offline indexing if it was paused and no pause for another pauseId is still present.<p> 2512 * @param pauseId the id of the pause request, which now allows for resuming. 2513 */ 2514 public void resumeOfflineIndexing(CmsUUID pauseId) { 2515 2516 synchronized (m_pauseRequests) { 2517 if (!m_pauseRequests.contains(pauseId)) { 2518 try { 2519 throw new IllegalArgumentException(); 2520 } catch (IllegalArgumentException e) { 2521 LOG.warn("Cannot resume for pause request " + pauseId + ". The request id is unknown.", e); 2522 } 2523 } else { 2524 m_pauseRequests.remove(pauseId); 2525 if (LOG.isDebugEnabled()) { 2526 LOG.debug( 2527 "Removed pause request " 2528 + pauseId 2529 + " from pause requests. Remaining pauses are: " 2530 + m_pauseRequests); 2531 } 2532 if (m_pauseRequests.isEmpty()) { 2533 LOG.info("Resuming offline indexing."); 2534 setOfflineUpdateFrequency( 2535 m_configuredOfflineIndexingFrequency > 0 2536 ? m_configuredOfflineIndexingFrequency 2537 : DEFAULT_OFFLINE_UPDATE_FREQNENCY); 2538 } 2539 } 2540 } 2541 } 2542 2543 /** 2544 * Sets the name of the directory below WEB-INF/ where the search indexes are stored.<p> 2545 * 2546 * @param value the name of the directory below WEB-INF/ where the search indexes are stored 2547 */ 2548 public void setDirectory(String value) { 2549 2550 m_path = value; 2551 } 2552 2553 /** 2554 * Sets the maximum age a text extraction result is kept in the cache (in hours).<p> 2555 * 2556 * @param extractionCacheMaxAge the maximum age for a text extraction result to set 2557 */ 2558 public void setExtractionCacheMaxAge(float extractionCacheMaxAge) { 2559 2560 m_extractionCacheMaxAge = extractionCacheMaxAge; 2561 } 2562 2563 /** 2564 * Sets the maximum age a text extraction result is kept in the cache (in hours) as a String.<p> 2565 * 2566 * @param extractionCacheMaxAge the maximum age for a text extraction result to set 2567 */ 2568 public void setExtractionCacheMaxAge(String extractionCacheMaxAge) { 2569 2570 try { 2571 setExtractionCacheMaxAge(Float.parseFloat(extractionCacheMaxAge)); 2572 } catch (NumberFormatException e) { 2573 LOG.error( 2574 Messages.get().getBundle().key( 2575 Messages.LOG_PARSE_EXTRACTION_CACHE_AGE_FAILED_2, 2576 extractionCacheMaxAge, 2577 Float.valueOf(DEFAULT_EXTRACTION_CACHE_MAX_AGE)), 2578 e); 2579 setExtractionCacheMaxAge(DEFAULT_EXTRACTION_CACHE_MAX_AGE); 2580 } 2581 } 2582 2583 /** 2584 * Sets the unlock mode during indexing.<p> 2585 * 2586 * @param value the value 2587 */ 2588 public void setForceunlock(String value) { 2589 2590 m_forceUnlockMode = CmsSearchForceUnlockMode.valueOf(value); 2591 } 2592 2593 /** 2594 * Sets the highlighter.<p> 2595 * 2596 * A highlighter is a class implementing org.opencms.search.documents.I_TermHighlighter.<p> 2597 * 2598 * @param highlighter the package/class name of the highlighter 2599 */ 2600 public void setHighlighter(String highlighter) { 2601 2602 try { 2603 m_highlighter = (I_CmsTermHighlighter)Class.forName(highlighter).newInstance(); 2604 } catch (Exception e) { 2605 m_highlighter = null; 2606 LOG.error(e.getLocalizedMessage(), e); 2607 } 2608 } 2609 2610 /** 2611 * Sets the seconds to wait for an index lock during an update operation.<p> 2612 * 2613 * @param value the seconds to wait for an index lock during an update operation 2614 */ 2615 public void setIndexLockMaxWaitSeconds(int value) { 2616 2617 m_indexLockMaxWaitSeconds = value; 2618 } 2619 2620 /** 2621 * Sets the max. excerpt length.<p> 2622 * 2623 * @param maxExcerptLength the max. excerpt length to set 2624 */ 2625 public void setMaxExcerptLength(int maxExcerptLength) { 2626 2627 m_maxExcerptLength = maxExcerptLength; 2628 } 2629 2630 /** 2631 * Sets the max. excerpt length as a String.<p> 2632 * 2633 * @param maxExcerptLength the max. excerpt length to set 2634 */ 2635 public void setMaxExcerptLength(String maxExcerptLength) { 2636 2637 try { 2638 setMaxExcerptLength(Integer.parseInt(maxExcerptLength)); 2639 } catch (Exception e) { 2640 LOG.error( 2641 Messages.get().getBundle().key( 2642 Messages.LOG_PARSE_EXCERPT_LENGTH_FAILED_2, 2643 maxExcerptLength, 2644 Integer.valueOf(DEFAULT_EXCERPT_LENGTH)), 2645 e); 2646 setMaxExcerptLength(DEFAULT_EXCERPT_LENGTH); 2647 } 2648 } 2649 2650 /** 2651 * Sets the maximal wait time for offline index updates after edit operations.<p> 2652 * 2653 * @param maxIndexWaitTime the maximal wait time to set in milliseconds 2654 */ 2655 public void setMaxIndexWaitTime(long maxIndexWaitTime) { 2656 2657 m_maxIndexWaitTime = maxIndexWaitTime; 2658 } 2659 2660 /** 2661 * Sets the maximal wait time for offline index updates after edit operations.<p> 2662 * 2663 * @param maxIndexWaitTime the maximal wait time to set in milliseconds 2664 */ 2665 public void setMaxIndexWaitTime(String maxIndexWaitTime) { 2666 2667 try { 2668 setMaxIndexWaitTime(Long.parseLong(maxIndexWaitTime)); 2669 } catch (Exception e) { 2670 LOG.error( 2671 Messages.get().getBundle().key( 2672 Messages.LOG_PARSE_MAX_INDEX_WAITTIME_FAILED_2, 2673 maxIndexWaitTime, 2674 Long.valueOf(DEFAULT_MAX_INDEX_WAITTIME)), 2675 e); 2676 setMaxIndexWaitTime(DEFAULT_MAX_INDEX_WAITTIME); 2677 } 2678 } 2679 2680 /** 2681 * Sets the maximum number of modifications before a commit in the search index is triggered.<p> 2682 * 2683 * @param maxModificationsBeforeCommit the maximum number of modifications to set 2684 */ 2685 public void setMaxModificationsBeforeCommit(int maxModificationsBeforeCommit) { 2686 2687 m_maxModificationsBeforeCommit = maxModificationsBeforeCommit; 2688 } 2689 2690 /** 2691 * Sets the maximum number of modifications before a commit in the search index is triggered as a string.<p> 2692 * 2693 * @param value the maximum number of modifications to set 2694 */ 2695 public void setMaxModificationsBeforeCommit(String value) { 2696 2697 try { 2698 setMaxModificationsBeforeCommit(Integer.parseInt(value)); 2699 } catch (Exception e) { 2700 LOG.error( 2701 Messages.get().getBundle().key( 2702 Messages.LOG_PARSE_MAXCOMMIT_FAILED_2, 2703 value, 2704 Integer.valueOf(DEFAULT_MAX_MODIFICATIONS_BEFORE_COMMIT)), 2705 e); 2706 setMaxModificationsBeforeCommit(DEFAULT_MAX_MODIFICATIONS_BEFORE_COMMIT); 2707 } 2708 } 2709 2710 /** 2711 * Sets the update frequency of the offline indexer in milliseconds.<p> 2712 * 2713 * @param offlineUpdateFrequency the update frequency in milliseconds to set 2714 */ 2715 public void setOfflineUpdateFrequency(long offlineUpdateFrequency) { 2716 2717 m_offlineUpdateFrequency = offlineUpdateFrequency; 2718 updateOfflineIndexes(0); 2719 } 2720 2721 /** 2722 * Sets the update frequency of the offline indexer in milliseconds.<p> 2723 * 2724 * @param offlineUpdateFrequency the update frequency in milliseconds to set 2725 */ 2726 public void setOfflineUpdateFrequency(String offlineUpdateFrequency) { 2727 2728 try { 2729 setOfflineUpdateFrequency(Long.parseLong(offlineUpdateFrequency)); 2730 } catch (Exception e) { 2731 LOG.error( 2732 Messages.get().getBundle().key( 2733 Messages.LOG_PARSE_OFFLINE_UPDATE_FAILED_2, 2734 offlineUpdateFrequency, 2735 Long.valueOf(DEFAULT_OFFLINE_UPDATE_FREQNENCY)), 2736 e); 2737 setOfflineUpdateFrequency(DEFAULT_OFFLINE_UPDATE_FREQNENCY); 2738 } 2739 } 2740 2741 /** 2742 * Sets the Solr configuration.<p> 2743 * 2744 * @param config the Solr configuration 2745 */ 2746 public void setSolrServerConfiguration(CmsSolrConfiguration config) { 2747 2748 m_solrConfig = config; 2749 } 2750 2751 /** 2752 * Sets the timeout to abandon threads indexing a resource.<p> 2753 * 2754 * @param value the timeout in milliseconds 2755 */ 2756 public void setTimeout(long value) { 2757 2758 m_timeout = value; 2759 } 2760 2761 /** 2762 * Sets the timeout to abandon threads indexing a resource as a String.<p> 2763 * 2764 * @param value the timeout in milliseconds 2765 */ 2766 public void setTimeout(String value) { 2767 2768 try { 2769 setTimeout(Long.parseLong(value)); 2770 } catch (Exception e) { 2771 LOG.error( 2772 Messages.get().getBundle().key( 2773 Messages.LOG_PARSE_TIMEOUT_FAILED_2, 2774 value, 2775 Long.valueOf(DEFAULT_TIMEOUT)), 2776 e); 2777 setTimeout(DEFAULT_TIMEOUT); 2778 } 2779 } 2780 2781 /** 2782 * Shuts down the search manager.<p> 2783 * 2784 * This will cause all search indices to be shut down.<p> 2785 */ 2786 public void shutDown() { 2787 2788 m_instantPublishIndexQueue.shutdown(); 2789 2790 if (m_offlineIndexThread != null) { 2791 m_offlineIndexThread.shutDown(); 2792 } 2793 2794 if (m_offlineHandler != null) { 2795 OpenCms.removeCmsEventListener(m_offlineHandler); 2796 } 2797 2798 Iterator<I_CmsSearchIndex> i = m_indexes.iterator(); 2799 while (i.hasNext()) { 2800 I_CmsSearchIndex index = i.next(); 2801 index.shutDown(); 2802 index = null; 2803 } 2804 m_indexes.clear(); 2805 2806 shutDownSolrContainer(); 2807 2808 if (CmsLog.INIT.isInfoEnabled()) { 2809 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_MANAGER_0)); 2810 } 2811 } 2812 2813 /** 2814 * Updates all offline indexes.<p> 2815 * 2816 * Can be used to force an index update when it's not convenient to wait until the 2817 * offline update interval has eclipsed.<p> 2818 * 2819 * Since the offline indexes still need some time to update the new resources, 2820 * the method waits for at most the configurable <code>maxIndexWaitTime</code> 2821 * to ensure that updating is finished. 2822 * 2823 * @see #updateOfflineIndexes(long) 2824 * 2825 */ 2826 public void updateOfflineIndexes() { 2827 2828 updateOfflineIndexes(getMaxIndexWaitTime()); 2829 } 2830 2831 /** 2832 * Updates all offline indexes.<p> 2833 * 2834 * Can be used to force an index update when it's not convenient to wait until the 2835 * offline update interval has eclipsed.<p> 2836 * 2837 * Since the offline index will still need some time to update the new resources even if it runs directly, 2838 * a wait time of 2500 or so should be given in order to make sure the index finished updating. 2839 * 2840 * @param waitTime milliseconds to wait after the offline update index was notified of the changes 2841 */ 2842 public void updateOfflineIndexes(long waitTime) { 2843 2844 if ((m_offlineIndexThread != null) && m_offlineIndexThread.isAlive()) { 2845 // notify existing thread of update frequency change 2846 if (LOG.isDebugEnabled()) { 2847 LOG.debug(Messages.get().getBundle().key(Messages.LOG_OI_UPDATE_INTERRUPT_0)); 2848 } 2849 m_offlineIndexThread.interrupt(); 2850 if (waitTime > 0) { 2851 m_offlineIndexThread.getWaitHandle().enter(waitTime); 2852 } 2853 } 2854 } 2855 2856 /** 2857 * Collects the resources whose indexed document depends on one of the updated resources.<p> 2858 * We take transitive dependencies into account and handle cyclic dependencies correctly as well. 2859 * 2860 * @param adminCms an OpenCms user context with Admin permissions 2861 * @param updateResources the resources to be re-indexed 2862 * 2863 * @return the updated list of resource to re-index 2864 */ 2865 protected List<CmsPublishedResource> addAdditionallyAffectedResources( 2866 CmsObject adminCms, 2867 List<CmsPublishedResource> updateResources) { 2868 2869 if (updateResources.size() > 0) { 2870 Set<CmsPublishedResource> updateResourceSet = new HashSet<>(updateResources); 2871 Collection<CmsPublishedResource> resourcesToCheck = updateResourceSet; 2872 Collection<CmsPublishedResource> additionalResources = Collections.emptySet(); 2873 do { 2874 additionalResources = findRelatedContainerPages(adminCms, updateResourceSet, resourcesToCheck); 2875 additionalResources.addAll( 2876 addIndexContentRelatedResources(adminCms, updateResourceSet, resourcesToCheck)); 2877 updateResources.addAll(additionalResources); 2878 updateResourceSet.addAll(additionalResources); 2879 resourcesToCheck = additionalResources; 2880 } while (resourcesToCheck.size() > 0); 2881 } 2882 return updateResources; 2883 } 2884 2885 /** 2886 * Collects the resources whose indexed document depends on one of the updated resources.<p> 2887 * 2888 * @param adminCms an OpenCms user context with Admin permissions 2889 * @param updateResources the resources to be re-indexed 2890 * @param updateResourcesToCheck the resources to check additionally affected resources for, subset of updateResources 2891 * 2892 * @return the list of resources that need to be additionally re-index 2893 */ 2894 protected Collection<CmsPublishedResource> addIndexContentRelatedResources( 2895 CmsObject adminCms, 2896 Collection<CmsPublishedResource> updateResources, 2897 Collection<CmsPublishedResource> updateResourcesToCheck) { 2898 2899 Collection<CmsPublishedResource> additionalUpdateResources = new HashSet<>(); 2900 for (CmsPublishedResource checkedRes : updateResourcesToCheck) { 2901 try { 2902 CmsRelationFilter filter = CmsRelationFilter.relationsToStructureId(checkedRes.getStructureId()); 2903 filter = filter.filterType(CmsRelationType.INDEX_CONTENT); 2904 List<CmsRelation> relations = adminCms.readRelations(filter); 2905 for (CmsRelation relation : relations) { 2906 CmsResource res = relation.getSource(adminCms, CmsResourceFilter.ALL); 2907 CmsPublishedResource additionalPubRes = new CmsPublishedResource(res); 2908 if (!updateResources.contains(additionalPubRes)) { 2909 additionalUpdateResources.add(additionalPubRes); 2910 } 2911 } 2912 } catch (CmsException e) { 2913 LOG.error(e.getLocalizedMessage(), e); 2914 } 2915 } 2916 return additionalUpdateResources; 2917 } 2918 2919 /** 2920 * Cleans up the extraction result cache.<p> 2921 */ 2922 protected void cleanExtractionCache() { 2923 2924 // clean up the extraction result cache 2925 m_extractionResultCache.cleanCache(m_extractionCacheMaxAge); 2926 } 2927 2928 /** 2929 * Collects the related containerpages to the resources that have been published.<p> 2930 * 2931 * @param adminCms an OpenCms user context with Admin permissions 2932 * @param updateResources the resources to be re-indexed 2933 * @param updateResourcesToCheck the resources to check additionally affected resources for, subset of updateResources 2934 * 2935 * @return the list of resources that need to be additionally re-index 2936 */ 2937 protected Collection<CmsPublishedResource> findRelatedContainerPages( 2938 CmsObject adminCms, 2939 Collection<CmsPublishedResource> updateResources, 2940 Collection<CmsPublishedResource> updateResourcesToCheck) { 2941 2942 CmsResourceManager resMan = OpenCms.getResourceManager(); 2943 Collection<CmsPublishedResource> additionalUpdateResources = new HashSet<>(); 2944 2945 Set<CmsResource> containerPages = new HashSet<CmsResource>(); 2946 int containerPageTypeId = -1; 2947 try { 2948 containerPageTypeId = CmsResourceTypeXmlContainerPage.getContainerPageTypeId(); 2949 } catch (CmsLoaderException e) { 2950 // will happen during setup, when container page type is not available yet 2951 LOG.info(e.getLocalizedMessage(), e); 2952 } 2953 if (containerPageTypeId != -1) { 2954 for (CmsPublishedResource pubRes : updateResourcesToCheck) { 2955 try { 2956 if (resMan.getResourceType(pubRes.getType()) instanceof CmsResourceTypeXmlContent) { 2957 if (!isGroup(pubRes.getType())) { 2958 CmsRelationFilter filter = CmsRelationFilter.relationsToStructureId( 2959 pubRes.getStructureId()).filterStrong(); 2960 List<CmsRelation> relations = adminCms.readRelations(filter); 2961 for (CmsRelation relation : relations) { 2962 CmsResource res = relation.getSource(adminCms, CmsResourceFilter.ALL); 2963 if (CmsResourceTypeXmlContainerPage.isContainerPage(res)) { 2964 containerPages.add(res); 2965 if (CmsDetailOnlyContainerUtil.isDetailContainersPage( 2966 adminCms, 2967 adminCms.getSitePath(res))) { 2968 addDetailContent(adminCms, containerPages, adminCms.getSitePath(res)); 2969 } 2970 } 2971 } 2972 } 2973 } 2974 if (containerPageTypeId == pubRes.getType()) { 2975 addDetailContent( 2976 adminCms, 2977 containerPages, 2978 adminCms.getRequestContext().removeSiteRoot(pubRes.getRootPath())); 2979 } 2980 } catch (CmsException e) { 2981 LOG.error(e.getLocalizedMessage(), e); 2982 } 2983 } 2984 // add all found container pages as published resource objects to the list 2985 for (CmsResource page : containerPages) { 2986 CmsPublishedResource pubCont = new CmsPublishedResource(page); 2987 if (!updateResources.contains(pubCont)) { 2988 // ensure container page is added only once 2989 additionalUpdateResources.add(pubCont); 2990 } 2991 } 2992 } 2993 return additionalUpdateResources; 2994 } 2995 2996 /** 2997 * Returns the set of names of all configured document types.<p> 2998 * 2999 * @return the set of names of all configured document types 3000 */ 3001 protected List<String> getDocumentTypes() { 3002 3003 return Collections.unmodifiableList(new ArrayList<String>(m_documentTypes.keySet())); 3004 } 3005 3006 /** 3007 * Returns the a offline project used for offline indexing.<p> 3008 * 3009 * @return the offline project if available 3010 */ 3011 protected CmsProject getOfflineIndexProject() { 3012 3013 CmsProject result = null; 3014 for (I_CmsSearchIndex index : m_offlineIndexes) { 3015 try { 3016 result = m_adminCms.readProject(index.getProject()); 3017 3018 if (!result.isOnlineProject()) { 3019 break; 3020 } 3021 } catch (Exception e) { 3022 // may be a missconfigured index, ignore 3023 LOG.error(e.getLocalizedMessage(), e); 3024 } 3025 } 3026 return result; 3027 } 3028 3029 /** 3030 * Returns a new thread manager for the indexing threads.<p> 3031 * 3032 * @return a new thread manager for the indexing threads 3033 */ 3034 protected CmsIndexingThreadManager getThreadManager() { 3035 3036 return new CmsIndexingThreadManager(m_timeout, m_maxModificationsBeforeCommit); 3037 } 3038 3039 /** 3040 * Initializes the available Cms resource types to be indexed.<p> 3041 * 3042 * A map stores document factories keyed by a string representing 3043 * a colon separated list of Cms resource types and/or mimetypes.<p> 3044 * 3045 * The keys of this map are used to trigger a document factory to convert 3046 * a Cms resource into a Lucene index document.<p> 3047 * 3048 * A document factory is a class implementing the interface 3049 * {@link org.opencms.search.documents.I_CmsDocumentFactory}.<p> 3050 */ 3051 protected void initAvailableDocumentTypes() { 3052 3053 CmsSearchDocumentType documenttype = null; 3054 String className = null; 3055 String name = null; 3056 I_CmsDocumentFactory documentFactory = null; 3057 List<String> resourceTypes = null; 3058 List<String> mimeTypes = null; 3059 CmsParameterConfiguration params = null; 3060 Class<?> c = null; 3061 3062 m_documentTypes = new LinkedHashMap<String, Map<String, I_CmsDocumentFactory>>(); 3063 3064 for (int i = 0, n = m_documentTypeConfigs.size(); i < n; i++) { 3065 3066 documenttype = m_documentTypeConfigs.get(i); 3067 name = documenttype.getName(); 3068 3069 try { 3070 className = documenttype.getClassName(); 3071 resourceTypes = documenttype.getResourceTypes(); 3072 mimeTypes = documenttype.getMimeTypes(); 3073 params = documenttype.getConfiguration(); 3074 3075 if (name == null) { 3076 throw new CmsIndexException(Messages.get().container(Messages.ERR_DOCTYPE_NO_NAME_0)); 3077 } 3078 if (className == null) { 3079 throw new CmsIndexException(Messages.get().container(Messages.ERR_DOCTYPE_NO_CLASS_DEF_0)); 3080 } 3081 if (resourceTypes.size() == 0) { 3082 throw new CmsIndexException(Messages.get().container(Messages.ERR_DOCTYPE_NO_RESOURCETYPE_DEF_0)); 3083 } 3084 3085 try { 3086 c = Class.forName(className); 3087 documentFactory = (I_CmsDocumentFactory)c.getConstructor(new Class[] {String.class}).newInstance( 3088 new Object[] {name}); 3089 } catch (ClassNotFoundException exc) { 3090 throw new CmsIndexException( 3091 Messages.get().container(Messages.ERR_DOCCLASS_NOT_FOUND_1, className), 3092 exc); 3093 } catch (Exception exc) { 3094 throw new CmsIndexException(Messages.get().container(Messages.ERR_DOCCLASS_INIT_1, className), exc); 3095 } 3096 if (params != null) { 3097 for (Entry<String, String> param : params.entrySet()) { 3098 documentFactory.addConfigurationParameter(param.getKey(), param.getValue()); 3099 } 3100 } 3101 3102 if (documentFactory.isUsingCache()) { 3103 // init cache if used by the factory 3104 documentFactory.setCache(m_extractionResultCache); 3105 } 3106 3107 Map<String, I_CmsDocumentFactory> matchingTypes = new HashMap<>(); 3108 for (Iterator<String> keyIt = documentFactory.getDocumentKeys( 3109 resourceTypes, 3110 mimeTypes).iterator(); keyIt.hasNext();) { 3111 String key = keyIt.next(); 3112 matchingTypes.put(key, documentFactory); 3113 m_extractionKeys.add(key); 3114 } 3115 m_documentTypes.put(name, matchingTypes); 3116 3117 } catch (CmsException e) { 3118 if (LOG.isWarnEnabled()) { 3119 LOG.warn(Messages.get().getBundle().key(Messages.LOG_DOCTYPE_CONFIG_FAILED_1, name), e); 3120 } 3121 } 3122 } 3123 } 3124 3125 /** 3126 * Initializes the index sources. 3127 */ 3128 protected void initIndexSources() { 3129 3130 for (CmsSearchIndexSource source : m_indexSources.values()) { 3131 source.init(); 3132 } 3133 } 3134 3135 /** 3136 * Initializes the configured search indexes.<p> 3137 * 3138 * This initializes also the list of Cms resources types 3139 * to be indexed by an index source.<p> 3140 */ 3141 protected void initSearchIndexes() { 3142 3143 I_CmsSearchIndex index = null; 3144 for (int i = 0, n = m_indexes.size(); i < n; i++) { 3145 index = m_indexes.get(i); 3146 // reset disabled flag 3147 index.setEnabled(true); 3148 // check if the index has been configured correctly 3149 if (index.checkConfiguration(m_adminCms)) { 3150 // the index is configured correctly 3151 try { 3152 index.initialize(); 3153 } catch (Exception e) { 3154 // disable the index - without this, an initialization failure that is not 3155 // handled inside initialize() itself leaves the index enabled and the 3156 // status report below wrongly logs it as successfully configured 3157 index.setEnabled(false); 3158 if (CmsLog.INIT.isWarnEnabled()) { 3159 // in this case the index will be disabled 3160 CmsLog.INIT.warn(Messages.get().getBundle().key(Messages.INIT_SEARCH_INIT_FAILED_1, index), e); 3161 } 3162 } 3163 } 3164 // output a log message if the index was successfully configured or not 3165 if (CmsLog.INIT.isInfoEnabled()) { 3166 if (index.isEnabled()) { 3167 CmsLog.INIT.info( 3168 Messages.get().getBundle().key(Messages.INIT_INDEX_CONFIGURED_2, index, index.getProject())); 3169 } else { 3170 CmsLog.INIT.warn( 3171 Messages.get().getBundle().key( 3172 Messages.INIT_INDEX_NOT_CONFIGURED_2, 3173 index, 3174 index.getProject())); 3175 } 3176 } 3177 } 3178 } 3179 3180 /** 3181 * Checks, if the index should be rebuilt/updated at all by the search manager. 3182 * @param index the index to check. 3183 * @return a flag, indicating if the index should be rebuilt/updated at all. 3184 */ 3185 protected boolean shouldUpdateAtAll(I_CmsSearchIndex index) { 3186 3187 if (I_CmsSearchIndex.REBUILD_MODE_NEVER.equals(index.getRebuildMode())) { 3188 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SKIP_REBUILD_FOR_MODE_NEVER_1, index.getName())); 3189 return false; 3190 } else { 3191 return true; 3192 } 3193 3194 } 3195 3196 /** 3197 * Incrementally updates all indexes that have their rebuild mode set to <code>"auto"</code> 3198 * after resources have been published.<p> 3199 * 3200 * @param adminCms an OpenCms user context with Admin permissions 3201 * @param publishHistoryId the history ID of the published project 3202 * @param report the report to write the output to 3203 */ 3204 protected void updateAllIndexes(CmsObject adminCms, CmsUUID publishHistoryId, I_CmsReport report) { 3205 3206 int oldPriority = Thread.currentThread().getPriority(); 3207 ONLINE_LOCK.lock(true); 3208 try { 3209 Thread.currentThread().setPriority(Thread.MIN_PRIORITY); 3210 List<CmsPublishedResource> publishedResources; 3211 try { 3212 // read the list of all published resources 3213 publishedResources = adminCms.readPublishedResources(publishHistoryId); 3214 } catch (CmsException e) { 3215 LOG.error( 3216 Messages.get().getBundle().key(Messages.LOG_READING_CHANGED_RESOURCES_FAILED_1, publishHistoryId), 3217 e); 3218 return; 3219 } 3220 List<CmsPublishedResource> updateResources = computeUpdateResources(adminCms, publishedResources); 3221 updateAllIndexes(adminCms, updateResources, report); 3222 } finally { 3223 ONLINE_LOCK.unlock(); 3224 Thread.currentThread().setPriority(oldPriority); 3225 } 3226 } 3227 3228 /** 3229 * Incrementally updates all indexes that have their rebuild mode set to <code>"auto"</code>.<p> 3230 * 3231 * @param adminCms an OpenCms user context with Admin permissions 3232 * @param updateResources the resources to update 3233 * @param report the report to write the output to 3234 */ 3235 protected void updateAllIndexes( 3236 CmsObject adminCms, 3237 List<CmsPublishedResource> updateResources, 3238 I_CmsReport report) { 3239 3240 try { 3241 ONLINE_LOCK.lock(true); 3242 if (!updateResources.isEmpty()) { 3243 // sort the resource to update 3244 Collections.sort(updateResources); 3245 // only update the indexes if the list of remaining published resources is not empty 3246 Iterator<I_CmsSearchIndex> i = m_indexes.iterator(); 3247 while (i.hasNext()) { 3248 I_CmsSearchIndex index = i.next(); 3249 if (I_CmsSearchIndex.REBUILD_MODE_AUTO.equals(index.getRebuildMode())) { 3250 // only update indexes which have the rebuild mode set to "auto" 3251 try { 3252 updateIndex(index, report, updateResources); 3253 } catch (CmsException e) { 3254 LOG.error( 3255 Messages.get().getBundle().key(Messages.LOG_UPDATE_INDEX_FAILED_1, index.getName()), 3256 e); 3257 } 3258 } 3259 } 3260 } 3261 // clean up the extraction result cache 3262 cleanExtractionCache(); 3263 } finally { 3264 ONLINE_LOCK.unlock(); 3265 } 3266 3267 } 3268 3269 /** 3270 * Updates (if required creates) the index with the given name.<p> 3271 * 3272 * If the optional List of <code>{@link CmsPublishedResource}</code> instances is provided, the index will be 3273 * incrementally updated for these resources only. If this List is <code>null</code> or empty, 3274 * the index will be fully rebuild.<p> 3275 * 3276 * @param index the index to update or rebuild 3277 * @param report the report to write output messages to 3278 * @param resourcesToIndex an (optional) list of <code>{@link CmsPublishedResource}</code> objects to update in the index 3279 * 3280 * @throws CmsException if something goes wrong 3281 */ 3282 protected void updateIndex(I_CmsSearchIndex index, I_CmsReport report, List<CmsPublishedResource> resourcesToIndex) 3283 throws CmsException { 3284 3285 if (shouldUpdateAtAll(index)) { 3286 CmsPriorityLock lock = I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode()) 3287 ? OFFLINE_LOCK 3288 : ONLINE_LOCK; 3289 try { 3290 lock.lock(true); 3291 3292 // copy the stored admin context for the indexing 3293 CmsObject cms = OpenCms.initCmsObject(m_adminCms); 3294 // make sure a report is available 3295 if (report == null) { 3296 report = new CmsLogReport(cms.getRequestContext().getLocale(), CmsSearchManager.class); 3297 } 3298 3299 // check if the index has been configured correctly 3300 if (!index.checkConfiguration(cms)) { 3301 // the index is disabled 3302 return; 3303 } 3304 3305 // set site root and project for this index 3306 cms.getRequestContext().setSiteRoot("/"); 3307 // switch to the index project 3308 cms.getRequestContext().setCurrentProject(cms.readProject(index.getProject())); 3309 3310 if ((resourcesToIndex == null) || resourcesToIndex.isEmpty()) { 3311 // rebuild the complete index 3312 3313 updateIndexCompletely(cms, index, report); 3314 } else { 3315 updateIndexIncremental(cms, index, report, resourcesToIndex); 3316 } 3317 } finally { 3318 lock.unlock(); 3319 } 3320 } 3321 } 3322 3323 /** 3324 * The method updates all OpenCms documents that are indexed. 3325 * @param cms the OpenCms user context to use for accessing the VFS 3326 * @param index the index to update 3327 * @param report the report to write output messages to 3328 * @throws CmsIndexException thrown if indexing fails for some reason 3329 */ 3330 @SuppressWarnings("null") 3331 protected void updateIndexCompletely(CmsObject cms, I_CmsSearchIndex index, I_CmsReport report) 3332 throws CmsIndexException { 3333 3334 // create a new thread manager for the indexing threads 3335 CmsIndexingThreadManager threadManager = getThreadManager(); 3336 3337 boolean isOfflineIndex = false; 3338 if (I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode())) { 3339 // disable offline indexing while the complete index is rebuild 3340 isOfflineIndex = true; 3341 index.setRebuildMode(I_CmsSearchIndex.REBUILD_MODE_MANUAL); 3342 // re-initialize the offline indexes, this will disable this offline index 3343 initOfflineIndexes(); 3344 } 3345 3346 I_CmsIndexWriter writer = null; 3347 try { 3348 // create a backup of the existing index 3349 CmsSearchIndex indexInternal = null; 3350 String backup = null; 3351 if (index instanceof CmsSearchIndex) { 3352 indexInternal = (CmsSearchIndex)index; 3353 backup = indexInternal.createIndexBackup(); 3354 if (backup != null) { 3355 indexInternal.indexSearcherOpen(backup); 3356 } 3357 } 3358 3359 // create a new index writer 3360 writer = index.getIndexWriter(report, true); 3361 if (writer instanceof I_CmsSolrIndexWriter) { 3362 try { 3363 ((I_CmsSolrIndexWriter)writer).deleteAllDocuments(); 3364 } catch (IOException e) { 3365 LOG.error(e.getMessage(), e); 3366 } 3367 } 3368 3369 // output start information on the report 3370 report.println( 3371 Messages.get().container(Messages.RPT_SEARCH_INDEXING_REBUILD_BEGIN_1, index.getName()), 3372 I_CmsReport.FORMAT_HEADLINE); 3373 3374 // iterate all configured index sources of this index 3375 Iterator<CmsSearchIndexSource> sources = index.getSources().iterator(); 3376 while (sources.hasNext()) { 3377 // get the next index source 3378 CmsSearchIndexSource source = sources.next(); 3379 // create the indexer 3380 I_CmsIndexer indexer = source.getIndexer().newInstance(cms, report, index); 3381 // new index creation, use all resources from the index source 3382 indexer.rebuildIndex(writer, threadManager, source); 3383 3384 // wait for indexing threads to finish 3385 while (threadManager.isRunning()) { 3386 try { 3387 Thread.sleep(500); 3388 } catch (InterruptedException e) { 3389 // just continue with the loop after interruption 3390 LOG.info(e.getLocalizedMessage(), e); 3391 } 3392 } 3393 3394 // commit and optimize the index after each index source has been finished 3395 try { 3396 writer.commit(); 3397 } catch (IOException e) { 3398 if (LOG.isWarnEnabled()) { 3399 LOG.warn( 3400 Messages.get().getBundle().key( 3401 Messages.LOG_IO_INDEX_WRITER_COMMIT_2, 3402 index.getName(), 3403 index.getPath()), 3404 e); 3405 } 3406 } 3407 try { 3408 writer.optimize(); 3409 } catch (IOException e) { 3410 if (LOG.isWarnEnabled()) { 3411 LOG.warn( 3412 Messages.get().getBundle().key( 3413 Messages.LOG_IO_INDEX_WRITER_OPTIMIZE_2, 3414 index.getName(), 3415 index.getPath()), 3416 e); 3417 } 3418 } 3419 } 3420 3421 // we are sure here that indexInternal is not null 3422 if (backup != null) { 3423 // remove the backup after the files have been re-indexed 3424 indexInternal.indexSearcherClose(); 3425 indexInternal.removeIndexBackup(backup); 3426 } 3427 3428 // output finish information on the report 3429 report.println( 3430 Messages.get().container(Messages.RPT_SEARCH_INDEXING_REBUILD_END_1, index.getName()), 3431 I_CmsReport.FORMAT_HEADLINE); 3432 3433 } finally { 3434 if (writer != null) { 3435 try { 3436 writer.close(); 3437 } catch (IOException e) { 3438 if (LOG.isWarnEnabled()) { 3439 LOG.warn( 3440 Messages.get().getBundle().key( 3441 Messages.LOG_IO_INDEX_WRITER_CLOSE_2, 3442 index.getPath(), 3443 index.getName()), 3444 e); 3445 } 3446 } 3447 } 3448 if (isOfflineIndex) { 3449 // reset the mode of the offline index 3450 index.setRebuildMode(I_CmsSearchIndex.REBUILD_MODE_OFFLINE); 3451 // re-initialize the offline indexes, this will re-enable this index 3452 initOfflineIndexes(); 3453 } 3454 // index has changed - initialize the index searcher instance 3455 index.onIndexChanged(true); 3456 } 3457 3458 // show information about indexing runtime 3459 threadManager.reportStatistics(report); 3460 } 3461 3462 /** 3463 * Incrementally updates the given index.<p> 3464 * 3465 * @param cms the OpenCms user context to use for accessing the VFS 3466 * @param index the index to update 3467 * @param report the report to write output messages to 3468 * @param resourcesToIndex a list of <code>{@link CmsPublishedResource}</code> objects to update in the index 3469 * 3470 * @throws CmsException if something goes wrong 3471 */ 3472 protected void updateIndexIncremental( 3473 CmsObject cms, 3474 I_CmsSearchIndex index, 3475 I_CmsReport report, 3476 List<CmsPublishedResource> resourcesToIndex) 3477 throws CmsException { 3478 3479 CmsPriorityLock lock = I_CmsSearchIndex.REBUILD_MODE_OFFLINE.equals(index.getRebuildMode()) 3480 ? OFFLINE_LOCK 3481 : ONLINE_LOCK; 3482 lock.lock(true); 3483 try { 3484 // update the existing index 3485 List<CmsSearchIndexUpdateData> updateCollections = new ArrayList<CmsSearchIndexUpdateData>(); 3486 3487 boolean hasResourcesToDelete = false; 3488 boolean hasResourcesToUpdate = false; 3489 3490 // iterate all configured index sources of this index 3491 Iterator<CmsSearchIndexSource> sources = index.getSources().iterator(); 3492 while (sources.hasNext()) { 3493 // get the next index source 3494 CmsSearchIndexSource source = sources.next(); 3495 // create the indexer 3496 I_CmsIndexer indexer = source.getIndexer().newInstance(cms, report, index); 3497 // collect the resources to update 3498 CmsSearchIndexUpdateData updateData = indexer.getUpdateData(source, resourcesToIndex); 3499 if (!updateData.isEmpty()) { 3500 // add the update collection to the internal pipeline 3501 updateCollections.add(updateData); 3502 hasResourcesToDelete = hasResourcesToDelete | updateData.hasResourcesToDelete(); 3503 hasResourcesToUpdate = hasResourcesToUpdate | updateData.hasResourceToUpdate(); 3504 } 3505 } 3506 3507 // only start index modification if required 3508 if (hasResourcesToDelete || hasResourcesToUpdate) { 3509 // output start information on the report 3510 report.println( 3511 Messages.get().container(Messages.RPT_SEARCH_INDEXING_UPDATE_BEGIN_1, index.getName()), 3512 I_CmsReport.FORMAT_HEADLINE); 3513 3514 I_CmsIndexWriter writer = null; 3515 try { 3516 // obtain an index writer that updates the current index 3517 writer = index.getIndexWriter(report, false); 3518 3519 if (hasResourcesToDelete) { 3520 // delete the resource from the index 3521 Iterator<CmsSearchIndexUpdateData> i = updateCollections.iterator(); 3522 while (i.hasNext()) { 3523 CmsSearchIndexUpdateData updateCollection = i.next(); 3524 if (updateCollection.hasResourcesToDelete()) { 3525 updateCollection.getIndexer().deleteResources( 3526 writer, 3527 updateCollection.getResourcesToDelete()); 3528 } 3529 } 3530 } 3531 3532 if (hasResourcesToUpdate) { 3533 // create a new thread manager 3534 CmsIndexingThreadManager threadManager = getThreadManager(); 3535 3536 Iterator<CmsSearchIndexUpdateData> i = updateCollections.iterator(); 3537 while (i.hasNext()) { 3538 CmsSearchIndexUpdateData updateCollection = i.next(); 3539 if (updateCollection.hasResourceToUpdate()) { 3540 updateCollection.getIndexer().updateResources( 3541 writer, 3542 threadManager, 3543 updateCollection.getResourcesToUpdate()); 3544 } 3545 } 3546 3547 // wait for indexing threads to finish 3548 while (threadManager.isRunning()) { 3549 try { 3550 Thread.sleep(500); 3551 } catch (InterruptedException e) { 3552 // just continue with the loop after interruption 3553 LOG.info(e.getLocalizedMessage(), e); 3554 } 3555 } 3556 } 3557 } finally { 3558 // close the index writer 3559 if (writer != null) { 3560 try { 3561 writer.commit(); 3562 } catch (IOException e) { 3563 LOG.error( 3564 Messages.get().getBundle().key( 3565 Messages.LOG_IO_INDEX_WRITER_COMMIT_2, 3566 index.getName(), 3567 index.getPath()), 3568 e); 3569 } 3570 } 3571 // index has changed - initialize the index searcher instance 3572 index.onIndexChanged(false); 3573 } 3574 3575 // output finish information on the report 3576 report.println( 3577 Messages.get().container(Messages.RPT_SEARCH_INDEXING_UPDATE_END_1, index.getName()), 3578 I_CmsReport.FORMAT_HEADLINE); 3579 } 3580 } finally { 3581 lock.unlock(); 3582 } 3583 } 3584 3585 /** 3586 * Updates the offline search indexes for the given list of resources.<p> 3587 * 3588 * @param report the report to write the index information to 3589 * @param resourcesToIndex the list of {@link CmsPublishedResource} objects to index 3590 */ 3591 protected void updateIndexOffline(I_CmsReport report, List<CmsPublishedResource> resourcesToIndex) { 3592 3593 CmsObject cms = m_adminCms; 3594 try { 3595 // copy the administration context for the indexing 3596 cms = OpenCms.initCmsObject(m_adminCms); 3597 // set site root and project for this index 3598 cms.getRequestContext().setSiteRoot("/"); 3599 } catch (CmsException e) { 3600 LOG.error(e.getLocalizedMessage(), e); 3601 } 3602 3603 Iterator<I_CmsSearchIndex> j = m_offlineIndexes.iterator(); 3604 while (j.hasNext()) { 3605 I_CmsSearchIndex index = j.next(); 3606 if (index.getSources() != null) { 3607 try { 3608 // switch to the index project 3609 cms.getRequestContext().setCurrentProject(cms.readProject(index.getProject())); 3610 updateIndexIncremental(cms, index, report, resourcesToIndex); 3611 } catch (CmsException e) { 3612 LOG.error(Messages.get().getBundle().key(Messages.LOG_UPDATE_INDEX_FAILED_1, index.getName()), e); 3613 } 3614 } 3615 } 3616 } 3617 3618 /** 3619 * Checks if the given containerpage is used as a detail containers and adds the related detail content to the resource set.<p> 3620 * 3621 * @param adminCms the cms context 3622 * @param containerPages the containerpages 3623 * @param containerPage the container page site path 3624 */ 3625 private void addDetailContent(CmsObject adminCms, Set<CmsResource> containerPages, String containerPage) { 3626 3627 if (CmsDetailOnlyContainerUtil.isDetailContainersPage(adminCms, containerPage)) { 3628 3629 try { 3630 CmsResource detailRes = adminCms.readResource( 3631 CmsDetailOnlyContainerUtil.getDetailContentPath(containerPage), 3632 CmsResourceFilter.IGNORE_EXPIRATION); 3633 containerPages.add(detailRes); 3634 } catch (Throwable e) { 3635 if (LOG.isWarnEnabled()) { 3636 LOG.warn(e.getLocalizedMessage(), e); 3637 } 3638 } 3639 } 3640 } 3641 3642 private List<CmsPublishedResource> computeUpdateResources( 3643 CmsObject cms, 3644 List<CmsPublishedResource> publishedResources) { 3645 3646 Set<CmsUUID> bothNewAndDeleted = getIdsOfPublishResourcesWhichAreBothNewAndDeleted(publishedResources); 3647 // When published resources with both states 'new' and 'deleted' exist in the same publish job history, the resource has been moved 3648 3649 List<CmsPublishedResource> updateResources = new ArrayList<CmsPublishedResource>(); 3650 for (CmsPublishedResource res : publishedResources) { 3651 if (res.getState().isUnchanged()) { 3652 // unchanged resources don't need to be indexed after publish 3653 continue; 3654 } 3655 if (res.getState().isDeleted() || res.getState().isNew() || res.getState().isChanged()) { 3656 if (updateResources.contains(res)) { 3657 // resource may have been added as a sibling of another resource 3658 // in this case we make sure to use the value from the publish list because of the "deleted" flag 3659 boolean hasMoved = bothNewAndDeleted.contains(res.getStructureId()) 3660 || (res.getMovedState() == CmsPublishedResource.STATE_MOVED_DESTINATION) 3661 || (res.getMovedState() == CmsPublishedResource.STATE_MOVED_SOURCE); 3662 // check it this is a moved resource with source / target info, in this case we need both entries 3663 if (!hasMoved) { 3664 // if the resource was moved, we must contain both entries 3665 updateResources.remove(res); 3666 } 3667 // "equals()" implementation of published resource checks for id, 3668 // so the removed value may have a different "deleted" or "modified" status value 3669 updateResources.add(res); 3670 } else { 3671 // resource not yet contained in the list 3672 updateResources.add(res); 3673 // check for the siblings (not for deleted resources, these are already gone) 3674 if (!res.getState().isDeleted() && (res.getSiblingCount() > 1)) { 3675 // this resource has siblings 3676 try { 3677 // read siblings from the online project 3678 List<CmsResource> siblings = cms.readSiblings(res.getRootPath(), CmsResourceFilter.ALL); 3679 Iterator<CmsResource> itSib = siblings.iterator(); 3680 while (itSib.hasNext()) { 3681 // check all siblings 3682 CmsResource sibling = itSib.next(); 3683 CmsPublishedResource sib = new CmsPublishedResource(sibling); 3684 if (!updateResources.contains(sib)) { 3685 // ensure sibling is added only once 3686 updateResources.add(sib); 3687 } 3688 } 3689 } catch (CmsException e) { 3690 // ignore, just use the original resource 3691 if (LOG.isWarnEnabled()) { 3692 LOG.warn( 3693 Messages.get().getBundle().key( 3694 Messages.LOG_UNABLE_TO_READ_SIBLINGS_1, 3695 res.getRootPath()), 3696 e); 3697 } 3698 } 3699 } 3700 } 3701 } 3702 } 3703 3704 addAdditionallyAffectedResources(cms, updateResources); 3705 return updateResources; 3706 } 3707 3708 /** 3709 * Creates the Solr core container.<p> 3710 * 3711 * @return the created core container 3712 */ 3713 private CoreContainer createCoreContainer() { 3714 3715 CoreContainer container = null; 3716 try { 3717 // get the core container 3718 // still no core container: create it 3719 container = CoreContainer.createAndLoad( 3720 Paths.get(m_solrConfig.getHome()), 3721 m_solrConfig.getSolrFile().toPath()); 3722 if (CmsLog.INIT.isInfoEnabled()) { 3723 CmsLog.INIT.info( 3724 Messages.get().getBundle().key( 3725 Messages.INIT_SOLR_CORE_CONTAINER_CREATED_2, 3726 m_solrConfig.getHome(), 3727 m_solrConfig.getSolrFile().getName())); 3728 } 3729 } catch (Exception e) { 3730 LOG.error( 3731 Messages.get().getBundle().key( 3732 Messages.ERR_SOLR_CORE_CONTAINER_NOT_CREATED_1, 3733 m_solrConfig.getSolrFile().getAbsolutePath()), 3734 e); 3735 } 3736 return container; 3737 3738 } 3739 3740 /** 3741 * Remove write.lock file in the data directory to ensure the index is unlocked. 3742 * @param dataDir the data directory of the Solr index that should be unlocked. 3743 */ 3744 private void ensureIndexIsUnlocked(String dataDir) { 3745 3746 Collection<File> lockFiles = new ArrayList<File>(2); 3747 lockFiles.add( 3748 new File( 3749 CmsFileUtil.addTrailingSeparator(CmsFileUtil.addTrailingSeparator(dataDir) + "index") + "write.lock")); 3750 lockFiles.add( 3751 new File( 3752 CmsFileUtil.addTrailingSeparator(CmsFileUtil.addTrailingSeparator(dataDir) + "spellcheck") 3753 + "write.lock")); 3754 for (File lockFile : lockFiles) { 3755 if (lockFile.exists()) { 3756 lockFile.delete(); 3757 LOG.warn( 3758 "Forcely unlocking index with data dir \"" 3759 + dataDir 3760 + "\" by removing file \"" 3761 + lockFile.getAbsolutePath() 3762 + "\"."); 3763 } 3764 } 3765 } 3766 3767 /** 3768 * Returns the report in the given event data, if <code>null</code> 3769 * a new log report is used.<p> 3770 * 3771 * @param event the event to get the report for 3772 * 3773 * @return the report 3774 */ 3775 private I_CmsReport getEventReport(CmsEvent event) { 3776 3777 I_CmsReport report = null; 3778 if (event.getData() != null) { 3779 report = (I_CmsReport)event.getData().get(I_CmsEventListener.KEY_REPORT); 3780 } 3781 if (report == null) { 3782 report = new CmsLogReport(Locale.ENGLISH, getClass()); 3783 } 3784 return report; 3785 } 3786 3787 /** 3788 * Gets all structure ids for which published resources of both states 'new' and 'deleted' exist in the given list.<p> 3789 * 3790 * @param publishedResources a list of published resources 3791 * 3792 * @return the set of structure ids that satisfy the condition above 3793 */ 3794 private Set<CmsUUID> getIdsOfPublishResourcesWhichAreBothNewAndDeleted( 3795 List<CmsPublishedResource> publishedResources) { 3796 3797 Set<CmsUUID> result = new HashSet<CmsUUID>(); 3798 Set<CmsUUID> deletedSet = new HashSet<CmsUUID>(); 3799 for (CmsPublishedResource pubRes : publishedResources) { 3800 if (pubRes.getState().isNew()) { 3801 result.add(pubRes.getStructureId()); 3802 } 3803 if (pubRes.getState().isDeleted()) { 3804 deletedSet.add(pubRes.getStructureId()); 3805 } 3806 } 3807 result.retainAll(deletedSet); 3808 return result; 3809 } 3810 3811 /** 3812 * Initializes the Solr client for external Solr servers 3813 * @param serverUrl the external server url 3814 * @return the solr client 3815 */ 3816 private SolrClient getSolrClient(String serverUrl) { 3817 3818 URI uri = URI.create(serverUrl); 3819 String basicAuthCredentials = uri.getUserInfo(); 3820 if (basicAuthCredentials == null) { 3821 basicAuthCredentials = CmsSecretUtil.getSecret(SECRET_KEY_SOLR_BASIC_AUTH_CREDENTIALS); 3822 } 3823 try { 3824 URI cleanUri = new URI( 3825 uri.getScheme(), 3826 null, 3827 uri.getHost(), 3828 uri.getPort(), 3829 uri.getPath(), 3830 uri.getQuery(), 3831 uri.getFragment()); 3832 serverUrl = cleanUri.toString(); 3833 } catch (URISyntaxException e) { 3834 if (CmsLog.INIT.isWarnEnabled()) { 3835 CmsLog.INIT.warn( 3836 "Failed to clean the provided external solr server url. Defaulting to the raw URL.", 3837 e); 3838 } 3839 } 3840 Builder clientBuilder = new Builder().withBaseSolrUrl(serverUrl).withConnectionTimeout( 3841 15, 3842 TimeUnit.SECONDS).withRequestTimeout(120, TimeUnit.SECONDS).useHttp1_1( 3843 true).withOptionalBasicAuthCredentials(basicAuthCredentials); 3844 3845 return clientBuilder.build(); 3846 3847 } 3848 3849 /** 3850 * Checks if the given type id belongs to a group type. 3851 * 3852 * @param type the type id to check 3853 * @return true if the type is a group type 3854 */ 3855 private boolean isGroup(int type) { 3856 3857 for (String groupType : groupTypes) { 3858 if (OpenCms.getResourceManager().matchResourceType(groupType, type)) { 3859 return true; 3860 } 3861 } 3862 return false; 3863 3864 } 3865 3866 /** 3867 * Shuts down the Solr core container.<p> 3868 */ 3869 private void shutDownSolrContainer() { 3870 3871 if (m_coreContainer != null) { 3872 for (SolrCore core : m_coreContainer.getCores()) { 3873 // do not unload spellcheck core because otherwise the core.properties file is removed 3874 // even when calling m_coreContainer.unload(core.getName(), false, false, false); 3875 if (!core.getName().equals(CmsSolrSpellchecker.SPELLCHECKER_INDEX_CORE)) { 3876 m_coreContainer.unload(core.getName(), false, false, true); 3877 } 3878 } 3879 m_coreContainer.shutdown(); 3880 if (CmsLog.INIT.isInfoEnabled()) { 3881 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SOLR_SHUTDOWN_SUCCESS_0)); 3882 } 3883 m_coreContainer = null; 3884 } 3885 } 3886 3887}