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