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