001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.search.galleries;
029
030import org.opencms.ade.configuration.CmsFunctionAvailability;
031import org.opencms.ade.galleries.shared.CmsGallerySearchScope;
032import org.opencms.configuration.preferences.CmsGallerySearchWordModePreference;
033import org.opencms.db.CmsUserSettings;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsPropertyDefinition;
036import org.opencms.file.types.CmsResourceTypeFunctionConfig;
037import org.opencms.i18n.CmsLocaleManager;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.search.A_CmsSearchIndex;
041import org.opencms.search.CmsSearchUtil;
042import org.opencms.search.fields.CmsSearchField;
043import org.opencms.search.fields.CmsSearchFieldConfiguration;
044import org.opencms.search.solr.CmsSolrQuery;
045import org.opencms.util.CmsPair;
046import org.opencms.util.CmsStringUtil;
047import org.opencms.util.CmsUUID;
048import org.opencms.xml.containerpage.CmsXmlDynamicFunctionHandler;
049
050import java.util.ArrayList;
051import java.util.Arrays;
052import java.util.Collection;
053import java.util.Collections;
054import java.util.List;
055import java.util.Locale;
056import java.util.Set;
057import java.util.stream.Collectors;
058
059import org.apache.commons.logging.Log;
060import org.apache.solr.client.solrj.request.SolrQuery.ORDER;
061
062import com.google.common.base.Joiner;
063
064/**
065 * Parameters used for the gallery search index.<p>
066 *
067 * @since 8.0.0
068 */
069public class CmsGallerySearchParameters {
070
071    /** Modes for the pre-processing of the search words entered by the user. */
072    public enum CmsGallerySearchWordMode {
073
074        /** Search for the words as sub-words, matching anywhere within a word. */
075        infix,
076
077        /** Search for the words as entered, without any pre-processing. */
078        plain;
079
080        /** The default search word mode. */
081        public static final CmsGallerySearchWordMode DEFAULT = plain;
082
083        /**
084         * Returns the search word mode matching the given configuration value.<p>
085         *
086         * In case the value does not match a search word mode, {@link #DEFAULT} is returned.<p>
087         *
088         * @param value the configuration value to look up
089         *
090         * @return the search word mode matching the given configuration value
091         */
092        public static CmsGallerySearchWordMode lookup(String value) {
093
094            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) {
095                try {
096                    return valueOf(value.trim());
097                } catch (IllegalArgumentException e) {
098                    LOG.warn("Invalid gallery search word mode: " + value + ". Using '" + DEFAULT + "' instead.");
099                }
100            }
101            return DEFAULT;
102        }
103    }
104
105    /** Sort parameter constants. */
106    public enum CmsGallerySortParam {
107
108        /** Sort by date created ascending. */
109        dateCreated_asc,
110
111        /** Sort by date created descending. */
112        dateCreated_desc,
113
114        /** Sort date expired ascending. */
115        dateExpired_asc,
116
117        /** Sort date expired descending. */
118        dateExpired_desc,
119
120        /** Sort by date modified ascending. */
121        dateLastModified_asc,
122
123        /** Sort by date modified descending. */
124        dateLastModified_desc,
125
126        /** Sort date released ascending. */
127        dateReleased_asc,
128
129        /** Sort date released descending. */
130        dateReleased_desc,
131
132        /** Sort by length ascending. */
133        length_asc,
134
135        /** Sort by length descending. */
136        length_desc,
137
138        /** Sort by VFS root path ascending. */
139        path_asc,
140
141        /** Sort by VFS root path descending. */
142        path_desc,
143
144        /** Sort by score descending. */
145        score,
146
147        /** Sort state ascending. */
148        state_asc,
149
150        /** Sort state descending. */
151        state_desc,
152
153        /** Sort by title ascending. */
154        title_asc,
155
156        /** Sort by title ascending. */
157        title_desc,
158
159        /** Sort by type ascending. */
160        type_asc,
161
162        /** Sort by type descending. */
163        type_desc,
164
165        /** Sort created by ascending. */
166        userCreated_asc,
167
168        /** Sort created by descending. */
169        userCreated_desc,
170
171        /** Sort modified by ascending. */
172        userLastModified_asc,
173
174        /** Sort modified by descending. */
175        userLastModified_desc;
176
177        /** The default sort parameter. */
178        public static final CmsGallerySortParam DEFAULT = title_asc;
179    }
180
181    /**
182     * Helper class to store a time range.<p>
183     */
184    class CmsGallerySearchTimeRange {
185
186        /** The end time of the time range. */
187        long m_endTime;
188
189        /** The start time of the time range. */
190        long m_startTime;
191
192        /**
193         * Default constructor.<p>
194         *
195         * This will create an object where the start date is equal to
196         * {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE}.<p>
197         */
198        public CmsGallerySearchTimeRange() {
199
200            m_startTime = Long.MIN_VALUE;
201            m_endTime = Long.MAX_VALUE;
202        }
203
204        /**
205         * Constructor with start and end time.<p>
206         *
207         * @param startTime the start time of the time range
208         * @param endTime the end time of the time range
209         */
210        public CmsGallerySearchTimeRange(long startTime, long endTime) {
211
212            m_startTime = startTime;
213            m_endTime = endTime;
214        }
215
216        /**
217         * Returns the end time of the time range.<p>
218         *
219         * @return the end time of the time range
220         */
221        public long getEndTime() {
222
223            return m_endTime;
224        }
225
226        /**
227         * Returns the start time of the time range.<p>
228         *
229         * @return the start time of the time range
230         */
231        public long getStartTime() {
232
233            return m_startTime;
234        }
235    }
236
237    /** Logge instance for this class. */
238    private static final Log LOG = CmsLog.getLog(CmsGallerySearchParameters.class);
239
240    /** The query operators that must not be extended by wildcards. */
241    private static final List<String> QUERY_OPERATORS = List.of("AND", "OR", "NOT");
242
243    /** The categories to search in. */
244    private List<String> m_categories;
245
246    /** The time range for the date of resource creation to consider in the search. */
247    private CmsGallerySearchTimeRange m_dateCreatedTimeRange;
248
249    /** The time range for the date of resource last modification to consider in the search. */
250    private CmsGallerySearchTimeRange m_dateLastModifiedTimeRange;
251
252    /** The set of functions to be excluded from the search result (may be null). */
253    private Set<CmsUUID> m_excludedFunctions;
254
255    /** The list of folders to search in. */
256    private List<String> m_folders;
257
258    /** Enlists all VFS folders to perform a search in. */
259    private List<String> m_foldersToSearchIn;
260
261    /** If true, empty search result should be returned regardless of other settings. */
262    private boolean m_forceEmptyResult;
263
264    /** Function availability. */
265    private CmsFunctionAvailability m_functionAvailability;
266
267    /** The galleries to search in. */
268    private List<String> m_galleries;
269
270    /** Indicates the search exclude property should be ignored. */
271    private boolean m_ignoreSearchExclude;
272
273    /** Set of functions to include (whitelist). */
274    private Set<CmsUUID> m_includedFunctions;
275
276    /** Indicates if expired and unreleased resources should be included in the search. */
277    private boolean m_includeExpired;
278
279    /** The locale for the search. */
280    private String m_locale;
281
282    /** The number of search results per page. */
283    private int m_matchesPerPage;
284
285    /** The sitemap reference path. */
286    private String m_referencePath;
287
288    /** The resource types to search for. */
289    private List<String> m_resourceTypes;
290
291    /** The requested page of the result. */
292    private int m_resultPage;
293
294    /** The gallery search scope. */
295    private CmsGallerySearchScope m_scope;
296
297    /** The sort order for the search result. */
298    private CmsGallerySortParam m_sortOrder;
299
300    /** The template compatibility. */
301    private String m_templateCompatibility;
302
303    /** Search words to search for. */
304    private String m_words;
305
306    /**
307     * Default constructor.<p>
308     */
309    public CmsGallerySearchParameters() {
310
311        m_resultPage = 1;
312        m_matchesPerPage = 10;
313    }
314
315    /**
316     * Returns the categories that have been included in the search.<p>
317     *
318     * If no categories have been set, then <code>null</code> is returned.<p>
319     *
320     * @return the categories that have been included in the search
321     */
322    public List<String> getCategories() {
323
324        return m_categories;
325    }
326
327    /**
328     * Returns the time range for the date of creation that has been used for the search result.<p>
329     *
330     * In case this time range has not been set, this will return an object
331     * where the start date is equal to {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE}.<p>
332     *
333     * @return the time range for the date of creation that has been used for the search result
334     */
335    public CmsGallerySearchTimeRange getDateCreatedRange() {
336
337        if (m_dateCreatedTimeRange == null) {
338            m_dateCreatedTimeRange = new CmsGallerySearchTimeRange();
339        }
340        return m_dateCreatedTimeRange;
341    }
342
343    /**
344     * Returns the time range for the dadelete examplete of last modification that has been used for the search result.<p>
345     *
346     * In case this time range has not been set, this will return an object
347     * where the start date is equal to {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE}.<p>
348     *
349     * @return the time range for the date of last modification that has been used for the search result
350     */
351    public CmsGallerySearchTimeRange getDateLastModifiedRange() {
352
353        if (m_dateLastModifiedTimeRange == null) {
354            m_dateLastModifiedTimeRange = new CmsGallerySearchTimeRange();
355        }
356        return m_dateLastModifiedTimeRange;
357    }
358
359    /**
360     * Gets the set of structure IDs of functions to exclude from the search result.
361     *
362     * @return the set of structure IDs to exclude
363     */
364    public Set<CmsUUID> getExcludedFunctions() {
365
366        return m_excludedFunctions;
367    }
368
369    /**
370     * Returns the list of folders to search in.<p>
371     *
372     * @return a list of paths of VFS folders
373     */
374    public List<String> getFolders() {
375
376        return m_folders;
377    }
378
379    /**
380     * Returns the galleries that have been included in the search.<p>
381     *
382     * If no galleries have been set, then <code>null</code> is returned.<p>
383     *
384     * @return the galleries that have been included in the search
385     */
386    public List<String> getGalleries() {
387
388        return m_galleries;
389    }
390
391    /**
392     * Gets the set of ids of functions to include.
393     *
394     * <p>Note: If the id of a function is returned in the ID set returned by thsi method,
395     * the function may still be excluded from search results based on other parameters.
396     *
397     * @return the included functions
398     */
399    public Set<CmsUUID> getIncludedFunctions() {
400
401        return m_includedFunctions;
402    }
403
404    /**
405     * Returns the locale that has been used for the search.<p>
406     *
407     * If no locale has been set, then <code>null</code> is returned.<p>
408     *
409     * @return the locale that has been used for the search
410     */
411    public String getLocale() {
412
413        if (m_locale == null) {
414            m_locale = CmsLocaleManager.getDefaultLocale().toString();
415        }
416        return m_locale;
417    }
418
419    /**
420     * Returns the maximum number of matches per result page.<p>
421     *
422     * @return the the maximum number of matches per result page
423     *
424     * @see #getMatchesPerPage()
425     * @see #setResultPage(int)
426     */
427    public int getMatchesPerPage() {
428
429        return m_matchesPerPage;
430    }
431
432    /**
433     * Returns a CmsSolrQuery representation of this class.
434     * @param cms the openCms object.
435     * @return CmsSolrQuery representation of this class.
436     */
437    public CmsSolrQuery getQuery(CmsObject cms) {
438
439        final CmsSolrQuery query = new CmsSolrQuery();
440
441        // set categories
442        query.setCategories(m_categories);
443
444        // Set date created time filter
445        query.addFilterQuery(
446            CmsSearchUtil.getDateCreatedTimeRangeFilterQuery(
447                CmsSearchField.FIELD_DATE_CREATED,
448                getDateCreatedRange().m_startTime,
449                getDateCreatedRange().m_endTime));
450
451        // Set date last modified time filter
452        query.addFilterQuery(
453            CmsSearchUtil.getDateCreatedTimeRangeFilterQuery(
454                CmsSearchField.FIELD_DATE_LASTMODIFIED,
455                getDateLastModifiedRange().m_startTime,
456                getDateLastModifiedRange().m_endTime));
457
458        // set scope / folders to search in
459        m_foldersToSearchIn = new ArrayList<>();
460        addFoldersToSearchIn(m_folders);
461        addFoldersToSearchIn(m_galleries);
462        setSearchFolders(cms);
463        query.setSearchRoots(new ArrayList<>(m_foldersToSearchIn));
464
465        if (!m_ignoreSearchExclude) {
466            // Reference for the values: CmsGallerySearchIndex.java, field EXCLUDE_PROPERTY_VALUES
467            query.addFilterQuery(
468                "-" + CmsSearchField.FIELD_SEARCH_EXCLUDE,
469                Arrays.asList(
470                    new String[] {
471                        A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_ALL,
472                        A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_GALLERY}),
473                false,
474                true);
475        }
476
477        // set matches per page
478        query.setRows(Integer.valueOf(m_matchesPerPage));
479
480        // set resource types
481        if (null != m_resourceTypes) {
482            List<String> resourceTypes = new ArrayList<>(m_resourceTypes);
483            if (m_resourceTypes.contains(CmsResourceTypeFunctionConfig.TYPE_NAME)
484                && !m_resourceTypes.contains(CmsXmlDynamicFunctionHandler.TYPE_FUNCTION)) {
485                resourceTypes.add(CmsXmlDynamicFunctionHandler.TYPE_FUNCTION);
486            }
487            query.setResourceTypes(resourceTypes);
488        }
489
490        // set result page
491        query.setStart(Integer.valueOf((m_resultPage - 1) * m_matchesPerPage));
492
493        // set search locale
494        if (null != m_locale) {
495            Locale l = CmsLocaleManager.getLocale(m_locale);
496            List<Locale> locales = new ArrayList<>(3);
497            locales.add(l);
498            if (!l.getVariant().isEmpty()) {
499                locales.add(Locale.of(l.getLanguage(), l.getCountry()));
500            }
501            if (!l.getCountry().isEmpty()) {
502                locales.add(Locale.of(l.getLanguage()));
503            }
504            query.setLocales(locales);
505        }
506
507        // set search words
508        if (null != m_words) {
509            query.setQuery(prepareSearchWords(m_words, getSearchWordMode(cms)));
510        }
511
512        // set sort order
513        query.setSort(getSort().getFirst(), getSort().getSecond());
514
515        // set result collapsing by id
516        // add sort criteria to possibly speed up performance and prevent sorting by score - not sure if a good solution
517        query.addFilterQuery(
518            "{!collapse field=id sort='"
519                + CmsSearchField.FIELD_INSTANCEDATE
520                + CmsSearchField.FIELD_POSTFIX_DATE
521                + " asc'}");
522
523        query.setFields(CmsGallerySearchResult.getRequiredSolrFields());
524
525        if ((m_functionAvailability != null) && m_functionAvailability.isDefined()) {
526            String notFunction = "(*:* AND -type:("
527                + CmsXmlDynamicFunctionHandler.TYPE_FUNCTION
528                + " OR "
529                + CmsResourceTypeFunctionConfig.TYPE_NAME
530                + "))";
531            Collection<CmsUUID> whitelist = m_functionAvailability.getWhitelist();
532            Collection<CmsUUID> blacklist = new ArrayList<>(m_functionAvailability.getBlacklist());
533            CmsUUID dummyId = CmsUUID.getNullUUID();
534            blacklist.add(dummyId);
535            String idClause;
536            if (whitelist != null) {
537                whitelist = new ArrayList<>(whitelist);
538                whitelist.add(dummyId);
539                String whitelistIdCondition = whitelist.stream().map(id -> id.toString()).collect(
540                    Collectors.joining(" OR "));
541                idClause = "id:(" + whitelistIdCondition + ")";
542            } else {
543                idClause = "*:* AND -id:("
544                    + blacklist.stream().map(id -> id.toString()).collect(Collectors.joining(" OR "))
545                    + ") ";
546            }
547            String functionFilter = "(" + notFunction + " OR (" + idClause + "))";
548            query.addFilterQuery(functionFilter);
549        }
550
551        if ((m_resourceTypes != null) && m_resourceTypes.contains(CmsResourceTypeFunctionConfig.TYPE_NAME)) {
552            if ((m_excludedFunctions != null) && (m_excludedFunctions.size() > 0)) {
553                List<CmsUUID> excludedFunctions = new ArrayList<>(m_excludedFunctions);
554                Collections.sort(excludedFunctions);
555                String orList = Joiner.on(" OR ").join(excludedFunctions);
556                String filter = "*:* AND -id:(" + orList + ")";
557                query.addFilterQuery(filter);
558            }
559            if (m_includedFunctions != null) {
560                List<CmsUUID> includedFunctions = new ArrayList<>(m_includedFunctions);
561                // not sure if order of terms matters for filter query caching in Solr, so normalize order just in case
562                Collections.sort(includedFunctions);
563                List<String> conditions = new ArrayList<>();
564                String notFunction = "(*:* AND -type:("
565                    + CmsXmlDynamicFunctionHandler.TYPE_FUNCTION
566                    + " OR "
567                    + CmsResourceTypeFunctionConfig.TYPE_NAME
568                    + "))";
569                conditions.add(notFunction);
570                for (CmsUUID id : includedFunctions) {
571                    conditions.add("id:" + id);
572                }
573                String includedFunctionsFilter = Joiner.on(" OR ").join(conditions);
574                query.addFilterQuery(includedFunctionsFilter);
575            }
576        }
577
578        if (m_templateCompatibility != null) {
579            if (m_templateCompatibility.matches("^[0-9a-zA-Z_]+$")) {
580                String fieldName = CmsPropertyDefinition.PROPERTY_TEMPLATE_COMPATILIBITY + "_prop";
581                query.addFilterQuery("(*:* NOT " + fieldName + ":*) OR " + fieldName + ":" + m_templateCompatibility);
582            } else {
583                LOG.warn(
584                    "Invalid template compatibility value: "
585                        + m_templateCompatibility
586                        + ". Must only contain digits, letters (a-z) or underscores.");
587            }
588        }
589
590        // include expired/unreleased
591        if (m_includeExpired) {
592            query.removeExpiration();
593        }
594
595        return query;
596    }
597
598    /**
599     * Gets the reference path.<p>
600     *
601     * @return the gallery reference path
602     */
603    public String getReferencePath() {
604
605        return m_referencePath;
606    }
607
608    /**
609     * Returns the names of the resource types that have been included in the search result.<p>
610     *
611     * If no resource types have been set, then <code>null</code> is returned.<p>
612     *
613     * @return the names of the resource types that have been included in the search result
614     */
615    public List<String> getResourceTypes() {
616
617        return m_resourceTypes;
618    }
619
620    /**
621     * Returns the index of the requested result page.<p>
622     *
623     * @return the index of the requested result page
624     *
625     * @see #setResultPage(int)
626     * @see #getMatchesPerPage()
627     * @see #setMatchesPerPage(int)
628     */
629    public int getResultPage() {
630
631        return m_resultPage;
632    }
633
634    /**
635     * The gallery search scope.<p>
636     *
637     * @return the gallery search scope
638     */
639    public CmsGallerySearchScope getScope() {
640
641        if (m_scope == null) {
642            return OpenCms.getWorkplaceManager().getGalleryDefaultScope();
643        }
644        return m_scope;
645    }
646
647    /**
648     * Returns the search word mode to use for the search words pre-processing.<p>
649     *
650     * The mode is taken from the user settings of the current user, with the
651     * system wide default configured in <code>opencms-workplace.xml</code> as fallback.<p>
652     *
653     * @param cms the current OpenCms user context
654     *
655     * @return the search word mode to use for the search words pre-processing
656     */
657    public CmsGallerySearchWordMode getSearchWordMode(CmsObject cms) {
658
659        String value = CmsUserSettings.getAdditionalPreference(
660            cms,
661            CmsGallerySearchWordModePreference.PREFERENCE_NAME,
662            true);
663        return CmsGallerySearchWordMode.lookup(value);
664    }
665
666    /**
667     * Returns the words (terms) that have been used for the full text search.<p>
668     *
669     * If no search words have been set, then <code>null</code> is returned.<p>
670     *
671     * @return the words (terms) that have been used for the full text search
672     */
673    public String getSearchWords() {
674
675        return m_words;
676    }
677
678    /**
679     * Returns the sort order that has been used in the search.<p>
680     *
681     * If the sort parameter has not been set the default sort order
682     * defined by {@link CmsGallerySortParam#DEFAULT} is used.<p>
683     *
684     * @return the sort order that has been used in the search
685     */
686    public CmsGallerySortParam getSortOrder() {
687
688        if (m_sortOrder == null) {
689
690            m_sortOrder = CmsGallerySortParam.DEFAULT;
691        }
692        return m_sortOrder;
693    }
694
695    /**
696     * Gets the template compatibility.
697     *
698     * <p>If set, matches those resources whose template.compatibility property is either empty or contains the value (possibly together with other values, separated by whitespace).
699     *
700     * @return the template compatibility
701     */
702    public String getTemplateCompatibility() {
703
704        return m_templateCompatibility;
705    }
706
707    /**
708     * If this returns true, an empty search result should be returned, regardless of other settings.
709     *
710     * @return true if an empty search result should be forced
711     */
712    public boolean isForceEmptyResult() {
713
714        return m_forceEmptyResult;
715    }
716
717    /**
718     * Returns the search exclude property ignore flag.<p>
719     *
720     * @return the search exclude property ignore flag
721     */
722    public boolean isIgnoreSearchExclude() {
723
724        return m_ignoreSearchExclude;
725    }
726
727    /**
728     * Returns a flag, indicating if release and expiration date should be ignored.<p>
729     *
730     * @return a flag, indicating if release and expiration date should be ignored
731     */
732    public boolean isIncludeExpired() {
733
734        return m_includeExpired;
735    }
736
737    /**
738     * Sets the categories for the search.<p>
739     *
740     * Results are found only if they are contained in at least one of the given categories.
741     *
742     * @param categories the categories to set
743     */
744    public void setCategories(List<String> categories) {
745
746        m_categories = categories;
747    }
748
749    /**
750     * Sets the time range for the date of resource creation to consider in the search.<p>
751     *
752     * @param startTime the start time of the time range
753     * @param endTime the end time of the time range
754     */
755    public void setDateCreatedTimeRange(long startTime, long endTime) {
756
757        if (m_dateCreatedTimeRange == null) {
758            m_dateCreatedTimeRange = new CmsGallerySearchTimeRange(startTime, endTime);
759        }
760    }
761
762    /**
763     * Sets the time range for the date of resource last modification to consider in the search.<p>
764     *
765     * @param startTime the start time of the time range
766     * @param endTime the end time of the time range
767     */
768    public void setDateLastModifiedTimeRange(long startTime, long endTime) {
769
770        if (m_dateLastModifiedTimeRange == null) {
771            m_dateLastModifiedTimeRange = new CmsGallerySearchTimeRange(startTime, endTime);
772        }
773    }
774
775    /**
776     * Sets the structure IDs of functions to exclude from the search results.
777     *
778     * @param excludedFunctions the structure IDs of functions to exclude
779     */
780    public void setExcludedFunctions(Set<CmsUUID> excludedFunctions) {
781
782        m_excludedFunctions = excludedFunctions;
783
784    }
785
786    /**
787     * Sets the folders to search in.<p>
788     *
789     * @param folders the list of VFS folders
790     */
791    public void setFolders(List<String> folders) {
792
793        m_folders = folders;
794    }
795
796    /**
797     * Enables/disables the 'force empty result' flag.
798     *
799     * If this is set to true, an empty search result should be returned regardless of the other parameters.
800     *
801     * @param forceEmptyResult if true, force an empty search result
802     */
803    public void setForceEmptyResult(boolean forceEmptyResult) {
804
805        m_forceEmptyResult = forceEmptyResult;
806    }
807
808    /**
809     * Sets the dynamic function availability.
810     *
811     * @param dynamicFunctionAvailability the dynamic function availability
812     */
813    public void setFunctionAvailability(CmsFunctionAvailability dynamicFunctionAvailability) {
814
815        m_functionAvailability = dynamicFunctionAvailability;
816    }
817
818    /**
819     * Sets the galleries for the search.<p>
820     *
821     * Results are found only if they are contained in one of the given galleries.
822     * If no gallery is set, results from all galleries will be returned in the search result.<p>
823     *
824     * @param galleries the galleries to set
825     */
826    public void setGalleries(List<String> galleries) {
827
828        m_galleries = galleries;
829    }
830
831    /**
832     * Sets the search exclude property ignore flag.<p>
833     *
834     * @param excludeForPageEditor the search exclude property ignore flag
835     */
836    public void setIgnoreSearchExclude(boolean excludeForPageEditor) {
837
838        m_ignoreSearchExclude = excludeForPageEditor;
839    }
840
841    /**
842     * Sets the ids of functions to include.
843     *
844     * @param includedFunctions the ids of functions to include
845     */
846    public void setIncludedFunctions(Set<CmsUUID> includedFunctions) {
847
848        m_includedFunctions = includedFunctions;
849    }
850
851    /**
852     * Set the flag, determining if expired and unreleased resources should be shown.
853     * @param includeExpired iff <code>true</code> expired and unreleased resources are shown.
854     */
855    public void setIncludeExpired(boolean includeExpired) {
856
857        m_includeExpired = includeExpired;
858
859    }
860
861    /**
862     * Sets the maximum number of matches per result page.<p>
863     *
864     * Use this together with {@link #setResultPage(int)} in order to split the result
865     * in more than one page.<p>
866     *
867     * @param matchesPerPage the the maximum number of matches per result page to set
868     *
869     * @see #getMatchesPerPage()
870     * @see #setResultPage(int)
871     */
872    public void setMatchesPerPage(int matchesPerPage) {
873
874        m_matchesPerPage = matchesPerPage;
875    }
876
877    /**
878     * Sets the gallery reference path.<p>
879     *
880     * @param referencePath the gallery reference path
881     */
882    public void setReferencePath(String referencePath) {
883
884        m_referencePath = referencePath;
885    }
886
887    /**
888     * Sets the names of the resource types to include in the search result.<p>
889     *
890     * Results are found only if they resources match one of the given resource type names.
891     * If no resource type name is set, all resource types will be returned in the search result.<p>
892     *
893     * @param resourceTypes the names of the resource types to include in the search result
894     */
895    public void setResourceTypes(List<String> resourceTypes) {
896
897        m_resourceTypes = resourceTypes;
898    }
899
900    /**
901     * Sets the index of the result page that should be returned.<p>
902     *
903     * Use this together with {@link #setMatchesPerPage(int)} in order to split the result
904     * in more than one page.<p>
905     *
906     * @param resultPage the index of the result page to return
907     *
908     * @see #getResultPage()
909     * @see #getMatchesPerPage()
910     * @see #setMatchesPerPage(int)
911     */
912    public void setResultPage(int resultPage) {
913
914        m_resultPage = resultPage;
915    }
916
917    /**
918     * Sets the search scope.<p>
919     *
920     * @param scope the search scope
921     */
922    public void setScope(CmsGallerySearchScope scope) {
923
924        m_scope = scope;
925    }
926
927    /**
928     * Sets the locale for the search.<p>
929     *
930     * Results are found only if they match the given locale.
931     * If no locale is set, results for all locales will be returned in the search result.<p>
932     *
933     * @param locale the locale to set
934     */
935    public void setSearchLocale(String locale) {
936
937        m_locale = locale;
938    }
939
940    /**
941     * Sets the words (terms) for the full text search.<p>
942     *
943     * Results are found only if they text extraction for the resource contains all given search words.
944     * If no search word is set, all resources will be returned in the search result.<p>
945     *
946     * Please note that this should be a list of words separated by white spaces.
947     * Simple Lucene modifiers such as (+), (-) and (*) are allowed, but anything more complex then this
948     * will be removed.<p>
949     *
950     * @param words the words (terms) for the full text search to set
951     */
952    public void setSearchWords(String words) {
953
954        m_words = words;
955    }
956
957    /**
958     * Sets the sort order for the search.<p>
959     *
960     * @param sortOrder the sort order to set
961     */
962    public void setSortOrder(CmsGallerySortParam sortOrder) {
963
964        m_sortOrder = sortOrder;
965    }
966
967    /**
968     * Sets the template compatibility string.
969     *
970     * @param compatibility the template compatibility string
971     */
972    public void setTemplateCompatibility(String compatibility) {
973
974        m_templateCompatibility = compatibility;
975    }
976
977    /**
978     * Pre-processes the search words entered by the user before they are used as Solr query.<p>
979     *
980     * Depending on the given mode, the individual words are extended by
981     * wildcards to allow a sub-word search.<p>
982     *
983     * @param words the search words as entered by the user
984     * @param mode the search word mode to apply
985     *
986     * @return the search words to use in the Solr query
987     */
988    protected String prepareSearchWords(String words, CmsGallerySearchWordMode mode) {
989
990        // We do not use any modifier when the search itself contains a modifier
991        // Quotes are checked here and not per word, since a phrase can span several words
992        if ((null == words) || words.isBlank() || words.contains("*") || words.contains("\"")) {
993            return words;
994        }
995        switch (mode) {
996            case infix:
997                return addInfixVariants(words);
998            case plain:
999            default:
1000                return words;
1001        }
1002    }
1003
1004    /**
1005     * Adds folders to perform the search in.
1006     * @param folders Folders to search in.
1007     */
1008    private void addFoldersToSearchIn(final List<String> folders) {
1009
1010        if (null == folders) {
1011            return;
1012        }
1013
1014        // the trailing slash required by the parent-folders index field is added by
1015        // CmsSolrQuery#setSearchRoots, through which this list reaches the query
1016        m_foldersToSearchIn.addAll(folders);
1017    }
1018
1019    /**
1020     * Adds the sub-word variants for an infix search to the given search words.<p>
1021     *
1022     * Each word is replaced by the group <code>(word OR word* OR *word OR *word*)</code>.
1023     * The variants with a leading wildcard are omitted for words that do not
1024     * start with a lowercase letter. Words that use a modifier are left alone.<p>
1025     *
1026     * The variants are grouped and combined with an explicit <code>OR</code> so that the
1027     * default operator of the index only applies between the words entered by the user.<p>
1028     *
1029     * @param words the search words as entered by the user
1030     *
1031     * @return the search words extended by the sub-word variants
1032     */
1033    private String addInfixVariants(String words) {
1034
1035        return Arrays.stream(words.trim().split("\\s+")).map(w -> {
1036            if (hasModifier(w)) {
1037                return w;
1038            }
1039            StringBuilder result = new StringBuilder("(").append(w).append(" OR ").append(w).append("*");
1040            if (Character.isLowerCase(w.charAt(0))) {
1041                result.append(" OR *").append(w).append(" OR *").append(w).append("*");
1042            }
1043            return result.append(")").toString();
1044        }).collect(Collectors.joining(" "));
1045    }
1046
1047    /**
1048     * Checks if the given list of resource type names contains a function-like type.<p>
1049     *
1050     * @param resourceTypes the collection of resource types
1051     * @return true if the list contains a function-like type
1052     */
1053    private boolean containsFunctionType(List<String> resourceTypes) {
1054
1055        if (resourceTypes.contains(CmsXmlDynamicFunctionHandler.TYPE_FUNCTION)) {
1056            return true;
1057        }
1058        if (resourceTypes.contains(CmsResourceTypeFunctionConfig.TYPE_NAME)) {
1059            return true;
1060        }
1061        return false;
1062    }
1063
1064    /**
1065     * Returns the Lucene sort indicated by the selected sort order.<p>
1066     *
1067     * @return the Lucene sort indicated by the selected sort order
1068     *
1069     * @see #getSortOrder()
1070     */
1071    private CmsPair<String, org.apache.solr.client.solrj.request.SolrQuery.ORDER> getSort() {
1072
1073        final String sortTitle = CmsSearchFieldConfiguration.getLocaleExtendedName(
1074            CmsSearchField.FIELD_DISPTITLE,
1075            getLocale()) + "_sort";
1076
1077        switch (getSortOrder()) {
1078            case dateCreated_asc:
1079                return CmsPair.create(CmsSearchField.FIELD_DATE_CREATED, ORDER.asc);
1080            case dateCreated_desc:
1081                return CmsPair.create(CmsSearchField.FIELD_DATE_CREATED, ORDER.desc);
1082            case dateExpired_asc:
1083                return CmsPair.create(CmsSearchField.FIELD_DATE_EXPIRED, ORDER.asc);
1084            case dateExpired_desc:
1085                return CmsPair.create(CmsSearchField.FIELD_DATE_EXPIRED, ORDER.desc);
1086            case dateLastModified_asc:
1087                return CmsPair.create(CmsSearchField.FIELD_DATE_LASTMODIFIED, ORDER.asc);
1088            case dateLastModified_desc:
1089                return CmsPair.create(CmsSearchField.FIELD_DATE_LASTMODIFIED, ORDER.desc);
1090            case dateReleased_asc:
1091                return CmsPair.create(CmsSearchField.FIELD_DATE_RELEASED, ORDER.asc);
1092            case dateReleased_desc:
1093                return CmsPair.create(CmsSearchField.FIELD_DATE_RELEASED, ORDER.desc);
1094            case length_asc:
1095                return CmsPair.create(CmsSearchField.FIELD_SIZE, ORDER.asc);
1096            case length_desc:
1097                return CmsPair.create(CmsSearchField.FIELD_SIZE, ORDER.desc);
1098            case path_asc:
1099                return CmsPair.create(CmsSearchField.FIELD_PATH, ORDER.asc);
1100            case path_desc:
1101                return CmsPair.create(CmsSearchField.FIELD_PATH, ORDER.desc);
1102            case score:
1103                return CmsPair.create(CmsSearchField.FIELD_SCORE, ORDER.desc);
1104            case state_asc:
1105                return CmsPair.create(CmsSearchField.FIELD_STATE, ORDER.asc);
1106            case state_desc:
1107                return CmsPair.create(CmsSearchField.FIELD_STATE, ORDER.desc);
1108            case title_asc:
1109                return CmsPair.create(sortTitle, ORDER.asc);
1110            case title_desc:
1111                return CmsPair.create(sortTitle, ORDER.desc);
1112            case type_asc:
1113                return CmsPair.create(CmsSearchField.FIELD_TYPE, ORDER.asc);
1114            case type_desc:
1115                return CmsPair.create(CmsSearchField.FIELD_TYPE, ORDER.desc);
1116            case userCreated_asc:
1117                return CmsPair.create(CmsSearchField.FIELD_USER_CREATED, ORDER.asc);
1118            case userCreated_desc:
1119                return CmsPair.create(CmsSearchField.FIELD_USER_CREATED, ORDER.desc);
1120            case userLastModified_asc:
1121                return CmsPair.create(CmsSearchField.FIELD_USER_LAST_MODIFIED, ORDER.asc);
1122            case userLastModified_desc:
1123                return CmsPair.create(CmsSearchField.FIELD_USER_LAST_MODIFIED, ORDER.desc);
1124            default:
1125                return CmsPair.create(sortTitle, ORDER.asc);
1126        }
1127    }
1128
1129    /**
1130     * Checks if the given search word uses a modifier.<p>
1131     *
1132     * Modifiers like <code>+word</code>, <code>-word</code>, <code>word~</code> or
1133     * <code>word^2</code> are placed before or after the word, so only words that do
1134     * not start and end with a letter are considered to use a modifier.
1135     * Note that a hyphen within a word, as in <code>Lkw-Fahrer</code>, is no modifier.<p>
1136     *
1137     * A field qualifier like <code>title:word</code> and the query operators
1138     * <code>AND</code>, <code>OR</code> and <code>NOT</code> are treated as modifiers, too.<p>
1139     *
1140     * @param word the search word to check
1141     *
1142     * @return <code>true</code> if the given search word uses a modifier
1143     */
1144    private boolean hasModifier(String word) {
1145
1146        return !Character.isLetter(word.charAt(0))
1147            || !Character.isLetter(word.charAt(word.length() - 1))
1148            || word.contains(":")
1149            || QUERY_OPERATORS.contains(word);
1150    }
1151
1152    /**
1153     * Applies the defined search folders to the Solr query.
1154     *
1155     * @param obj The current CmsObject object.
1156     */
1157    private void setSearchFolders(CmsObject obj) {
1158
1159        // check if parentFolders to search in have been set
1160        // if this evaluates false, the search folders have already been set, so
1161        // there's no need to add a scope filter
1162        if (m_foldersToSearchIn.isEmpty()) {
1163            // only append scope filter if no no folders or galleries given
1164            setSearchScopeFilter(obj);
1165        }
1166    }
1167
1168    /**
1169     * Sets the search scope.
1170     *
1171     * @param cms The current CmsObject object.
1172     */
1173    private void setSearchScopeFilter(CmsObject cms) {
1174
1175        final List<String> searchRoots = CmsSearchUtil.computeScopeFolders(cms, this);
1176
1177        // If the resource types contain the type "function" also
1178        // add "/system/modules/" to the search path
1179
1180        if ((null != getResourceTypes()) && containsFunctionType(getResourceTypes())) {
1181            searchRoots.add("/system/modules/");
1182        }
1183
1184        addFoldersToSearchIn(searchRoots);
1185    }
1186}