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