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 &lt;analyzer&gt;. */
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}