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