xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/web/SearchHelper.java (revision d6df19e1b22784c78f567cf74c42f18e3901b900)
1b5840353SAdam Hornáček /*
2b5840353SAdam Hornáček  * CDDL HEADER START
3b5840353SAdam Hornáček  *
4b5840353SAdam Hornáček  * The contents of this file are subject to the terms of the
5b5840353SAdam Hornáček  * Common Development and Distribution License (the "License").
6b5840353SAdam Hornáček  * You may not use this file except in compliance with the License.
7b5840353SAdam Hornáček  *
8b5840353SAdam Hornáček  * See LICENSE.txt included in this distribution for the specific
9b5840353SAdam Hornáček  * language governing permissions and limitations under the License.
10b5840353SAdam Hornáček  *
11b5840353SAdam Hornáček  * When distributing Covered Code, include this CDDL HEADER in each
12b5840353SAdam Hornáček  * file and include the License file at LICENSE.txt.
13b5840353SAdam Hornáček  * If applicable, add the following below this CDDL HEADER, with the
14b5840353SAdam Hornáček  * fields enclosed by brackets "[]" replaced with your own identifying
15b5840353SAdam Hornáček  * information: Portions Copyright [yyyy] [name of copyright owner]
16b5840353SAdam Hornáček  *
17b5840353SAdam Hornáček  * CDDL HEADER END
18b5840353SAdam Hornáček  */
19b5840353SAdam Hornáček 
20b5840353SAdam Hornáček /*
2138625bbaSVladimir Kotal  * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
225d9f3aa0SAdam Hornáček  * Portions Copyright (c) 2011, Jens Elkner.
235d9f3aa0SAdam Hornáček  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24b5840353SAdam Hornáček  */
259805b761SAdam Hornáček package org.opengrok.indexer.web;
26b5840353SAdam Hornáček 
27b5840353SAdam Hornáček import java.io.File;
28b5840353SAdam Hornáček import java.io.FileNotFoundException;
29b5840353SAdam Hornáček import java.io.IOException;
307d004396SChris Fraire import java.nio.file.Path;
317d004396SChris Fraire import java.nio.file.Paths;
32b5840353SAdam Hornáček import java.util.ArrayList;
33b5840353SAdam Hornáček import java.util.List;
34b5840353SAdam Hornáček import java.util.Map;
35b5840353SAdam Hornáček import java.util.Set;
36b5840353SAdam Hornáček import java.util.SortedSet;
37b5840353SAdam Hornáček import java.util.TreeSet;
38b5840353SAdam Hornáček import java.util.logging.Level;
39b5840353SAdam Hornáček import java.util.logging.Logger;
40b5840353SAdam Hornáček import java.util.regex.Pattern;
41b5840353SAdam Hornáček import java.util.stream.Collectors;
42b5840353SAdam Hornáček import org.apache.lucene.document.Document;
43b5840353SAdam Hornáček import org.apache.lucene.index.DirectoryReader;
44b5840353SAdam Hornáček import org.apache.lucene.index.IndexReader;
45c1c35457SChris Fraire import org.apache.lucene.index.IndexableField;
46eeb95924SChris Fraire import org.apache.lucene.index.LeafReaderContext;
47eeb95924SChris Fraire import org.apache.lucene.index.ReaderUtil;
48b5840353SAdam Hornáček import org.apache.lucene.index.Term;
49b5840353SAdam Hornáček import org.apache.lucene.queryparser.classic.ParseException;
50b5840353SAdam Hornáček import org.apache.lucene.search.IndexSearcher;
51eeb95924SChris Fraire import org.apache.lucene.search.Matches;
52eeb95924SChris Fraire import org.apache.lucene.search.MatchesIterator;
53eeb95924SChris Fraire import org.apache.lucene.search.MatchesUtils;
54b5840353SAdam Hornáček import org.apache.lucene.search.Query;
55b5840353SAdam Hornáček import org.apache.lucene.search.ScoreDoc;
56eeb95924SChris Fraire import org.apache.lucene.search.ScoreMode;
57b5840353SAdam Hornáček import org.apache.lucene.search.Sort;
58b5840353SAdam Hornáček import org.apache.lucene.search.SortField;
59b5840353SAdam Hornáček import org.apache.lucene.search.TermQuery;
60b5840353SAdam Hornáček import org.apache.lucene.search.TopDocs;
61b5840353SAdam Hornáček import org.apache.lucene.search.TopFieldDocs;
62eeb95924SChris Fraire import org.apache.lucene.search.Weight;
63b5840353SAdam Hornáček import org.apache.lucene.search.spell.DirectSpellChecker;
64b5840353SAdam Hornáček import org.apache.lucene.search.spell.SuggestMode;
65b5840353SAdam Hornáček import org.apache.lucene.search.spell.SuggestWord;
66b5840353SAdam Hornáček import org.apache.lucene.store.FSDirectory;
67eeb95924SChris Fraire import org.opengrok.indexer.analysis.AbstractAnalyzer;
689805b761SAdam Hornáček import org.opengrok.indexer.analysis.AnalyzerGuru;
699805b761SAdam Hornáček import org.opengrok.indexer.analysis.CompatibleAnalyser;
709805b761SAdam Hornáček import org.opengrok.indexer.analysis.Definitions;
719805b761SAdam Hornáček import org.opengrok.indexer.configuration.Project;
729805b761SAdam Hornáček import org.opengrok.indexer.configuration.RuntimeEnvironment;
739805b761SAdam Hornáček import org.opengrok.indexer.configuration.SuperIndexSearcher;
749805b761SAdam Hornáček import org.opengrok.indexer.index.IndexDatabase;
757d004396SChris Fraire import org.opengrok.indexer.index.IndexedSymlink;
769805b761SAdam Hornáček import org.opengrok.indexer.logger.LoggerFactory;
779805b761SAdam Hornáček import org.opengrok.indexer.search.QueryBuilder;
78497eb8c9SChris Fraire import org.opengrok.indexer.search.SettingsHelper;
799805b761SAdam Hornáček import org.opengrok.indexer.search.Summarizer;
809805b761SAdam Hornáček import org.opengrok.indexer.search.context.Context;
819805b761SAdam Hornáček import org.opengrok.indexer.search.context.HistoryContext;
829805b761SAdam Hornáček import org.opengrok.indexer.util.ForbiddenSymlinkException;
839805b761SAdam Hornáček import org.opengrok.indexer.util.IOUtils;
84b5840353SAdam Hornáček 
85b5840353SAdam Hornáček /**
86b5840353SAdam Hornáček  * Working set for a search basically to factor out/separate search related
87b5840353SAdam Hornáček  * complexity from UI design.
88b5840353SAdam Hornáček  *
89b5840353SAdam Hornáček  * @author Jens Elkner
90b5840353SAdam Hornáček  */
91b5840353SAdam Hornáček public class SearchHelper {
92b5840353SAdam Hornáček 
93b5840353SAdam Hornáček     private static final Logger LOGGER = LoggerFactory.getLogger(SearchHelper.class);
94b5840353SAdam Hornáček 
95ae1c323bSVladimir Kotal     private static final Pattern TAB_SPACE = Pattern.compile("[\t ]+");
96ae1c323bSVladimir Kotal 
97b5840353SAdam Hornáček     public static final String REQUEST_ATTR = "SearchHelper";
98ae1c323bSVladimir Kotal 
99ae1c323bSVladimir Kotal     /**
100ae1c323bSVladimir Kotal      * Default query parse error message prefix.
101ae1c323bSVladimir Kotal      */
102ae1c323bSVladimir Kotal     public static final String PARSE_ERROR_MSG = "Unable to parse your query: ";
103ae1c323bSVladimir Kotal 
104b5840353SAdam Hornáček     /**
105ff44f24aSAdam Hornáček      * Max number of words to suggest for spellcheck.
106b5840353SAdam Hornáček      */
107ae1c323bSVladimir Kotal     public static final int SPELLCHECK_SUGGEST_WORD_COUNT = 5;
108ae1c323bSVladimir Kotal 
109b5840353SAdam Hornáček     /**
110ae1c323bSVladimir Kotal      * data root: used to find the search index file.
111b5840353SAdam Hornáček      */
112ae1c323bSVladimir Kotal     private final File dataRoot;
113b5840353SAdam Hornáček     /**
114ae1c323bSVladimir Kotal      * context path, i.e. the applications' context path (usually /source) to use
115b5840353SAdam Hornáček      * when generating a redirect URL
116b5840353SAdam Hornáček      */
117ae1c323bSVladimir Kotal     private final String contextPath;
118ae1c323bSVladimir Kotal 
119b5840353SAdam Hornáček     /**
120b5840353SAdam Hornáček      * piggyback: the source root directory.
121b5840353SAdam Hornáček      */
122ae1c323bSVladimir Kotal     private final File sourceRoot;
123ae1c323bSVladimir Kotal 
124b5840353SAdam Hornáček     /**
125ae1c323bSVladimir Kotal      * piggyback: the <i>Eftar</i> file-reader to use.
126b5840353SAdam Hornáček      */
127ae1c323bSVladimir Kotal     private final EftarFileReader desc;
128b5840353SAdam Hornáček     /**
129b5840353SAdam Hornáček      * the result cursor start index, i.e. where to start displaying results
130b5840353SAdam Hornáček      */
131ae1c323bSVladimir Kotal     private final int start;
132b5840353SAdam Hornáček     /**
133b5840353SAdam Hornáček      * max. number of result items to show
134b5840353SAdam Hornáček      */
135ae1c323bSVladimir Kotal     private final int maxItems;
136b5840353SAdam Hornáček     /**
137ff44f24aSAdam Hornáček      * The QueryBuilder used to create the query.
138b5840353SAdam Hornáček      */
139ae1c323bSVladimir Kotal     private final QueryBuilder builder;
140b5840353SAdam Hornáček     /**
141ff44f24aSAdam Hornáček      * The order used for ordering query results.
142b5840353SAdam Hornáček      */
143ae1c323bSVladimir Kotal     private final SortOrder order;
144b5840353SAdam Hornáček     /**
145ae1c323bSVladimir Kotal      * Indicate whether this is search from a cross-reference. If {@code true}
146b5840353SAdam Hornáček      * {@link #executeQuery()} sets {@link #redirect} if certain conditions are
147b5840353SAdam Hornáček      * met.
148b5840353SAdam Hornáček      */
149ae1c323bSVladimir Kotal     private final boolean crossRefSearch;
150b5840353SAdam Hornáček     /**
151ae1c323bSVladimir Kotal      * As with {@link #crossRefSearch}, but here indicating either a
152eeb95924SChris Fraire      * cross-reference search or a "full blown search".
153eeb95924SChris Fraire      */
154ae1c323bSVladimir Kotal     private final boolean guiSearch;
155eeb95924SChris Fraire     /**
156b5840353SAdam Hornáček      * if not {@code null}, the consumer should redirect the client to a
157b5840353SAdam Hornáček      * separate result page denoted by the value of this field. Automatically
158b5840353SAdam Hornáček      * set via {@link #prepareExec(SortedSet)} and {@link #executeQuery()}.
159b5840353SAdam Hornáček      */
160ae1c323bSVladimir Kotal     private String redirect;
161b5840353SAdam Hornáček     /**
16299167638SChris Fraire      * A value indicating if redirection should be short-circuited when state or
16399167638SChris Fraire      * query result would have indicated otherwise.
16499167638SChris Fraire      */
165ae1c323bSVladimir Kotal     private final boolean noRedirect;
16699167638SChris Fraire     /**
167b5840353SAdam Hornáček      * if not {@code null}, the UI should show this error message and stop
168b5840353SAdam Hornáček      * processing the search. Automatically set via
169b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)} and {@link #executeQuery()}.
170b5840353SAdam Hornáček      */
171ae1c323bSVladimir Kotal     private String errorMsg;
172b5840353SAdam Hornáček     /**
173b5840353SAdam Hornáček      * the reader used to open the index. Automatically set via
174b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)}.
175b5840353SAdam Hornáček      */
176b5840353SAdam Hornáček     private IndexReader reader;
177b5840353SAdam Hornáček     /**
178b5840353SAdam Hornáček      * the searcher used to open/search the index. Automatically set via
179b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)}.
180b5840353SAdam Hornáček      */
181ae1c323bSVladimir Kotal     private IndexSearcher searcher;
182b5840353SAdam Hornáček     /**
183b5840353SAdam Hornáček      * If performing multi-project search, the indexSearcher objects will be
184b5840353SAdam Hornáček      * tracked by the indexSearcherMap so that they can be properly released
185b5840353SAdam Hornáček      * once the results are read.
186b5840353SAdam Hornáček      */
187b5840353SAdam Hornáček     private final ArrayList<SuperIndexSearcher> searcherList = new ArrayList<>();
188b5840353SAdam Hornáček     /**
189ff44f24aSAdam Hornáček      * Close IndexReader associated with searches on destroy().
190b5840353SAdam Hornáček      */
191b5840353SAdam Hornáček     private Boolean closeOnDestroy;
192b5840353SAdam Hornáček     /**
193ff44f24aSAdam Hornáček      * List of docs which result from the executing the query.
194b5840353SAdam Hornáček      */
195ae1c323bSVladimir Kotal     private ScoreDoc[] hits;
196b5840353SAdam Hornáček     /**
197ff44f24aSAdam Hornáček      * Total number of hits.
198b5840353SAdam Hornáček      */
199ae1c323bSVladimir Kotal     private long totalHits;
200b5840353SAdam Hornáček     /**
201b5840353SAdam Hornáček      * the query created by {@link #builder} via
202b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)}.
203b5840353SAdam Hornáček      */
204ae1c323bSVladimir Kotal     private Query query;
205b5840353SAdam Hornáček     /**
206b5840353SAdam Hornáček      * the Lucene sort instruction based on {@link #order} created via
207b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)}.
208b5840353SAdam Hornáček      */
209b5840353SAdam Hornáček     protected Sort sort;
210b5840353SAdam Hornáček     /**
211ff44f24aSAdam Hornáček      * The spellchecker object.
212b5840353SAdam Hornáček      */
2137d004396SChris Fraire     private DirectSpellChecker checker;
214b5840353SAdam Hornáček     /**
215b5840353SAdam Hornáček      * projects to use to setup indexer searchers. Usually setup via
216b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)}.
217b5840353SAdam Hornáček      */
218ae1c323bSVladimir Kotal     private SortedSet<String> projects;
219b5840353SAdam Hornáček     /**
220b5840353SAdam Hornáček      * opengrok summary context. Usually created via {@link #prepareSummary()}.
221b5840353SAdam Hornáček      */
222ae1c323bSVladimir Kotal     private Context sourceContext = null;
223b5840353SAdam Hornáček     /**
224b5840353SAdam Hornáček      * result summarizer usually created via {@link #prepareSummary()}.
225b5840353SAdam Hornáček      */
226ae1c323bSVladimir Kotal     private Summarizer summarizer = null;
227b5840353SAdam Hornáček     /**
228b5840353SAdam Hornáček      * history context usually created via {@link #prepareSummary()}.
229b5840353SAdam Hornáček      */
230ae1c323bSVladimir Kotal     private HistoryContext historyContext;
231b5840353SAdam Hornáček 
232ae1c323bSVladimir Kotal     private File indexDir;
233b5840353SAdam Hornáček 
234497eb8c9SChris Fraire     private SettingsHelper settingsHelper;
235b5840353SAdam Hornáček 
SearchHelper(int start, SortOrder sortOrder, File dataRoot, File sourceRoot, int maxItems, EftarFileReader eftarFileReader, QueryBuilder queryBuilder, boolean crossRefSearch, String contextPath, boolean guiSearch, boolean noRedirect)236ae1c323bSVladimir Kotal     public SearchHelper(int start, SortOrder sortOrder, File dataRoot, File sourceRoot, int maxItems,
237ae1c323bSVladimir Kotal                         EftarFileReader eftarFileReader, QueryBuilder queryBuilder, boolean crossRefSearch,
238ae1c323bSVladimir Kotal                         String contextPath, boolean guiSearch, boolean noRedirect) {
239ae1c323bSVladimir Kotal         this.start = start;
240ae1c323bSVladimir Kotal         this.order = sortOrder;
241ae1c323bSVladimir Kotal         this.dataRoot = dataRoot;
242ae1c323bSVladimir Kotal         this.sourceRoot = sourceRoot;
243ae1c323bSVladimir Kotal         this.maxItems = maxItems;
244ae1c323bSVladimir Kotal         this.desc = eftarFileReader;
245ae1c323bSVladimir Kotal         this.builder = queryBuilder;
246ae1c323bSVladimir Kotal         this.crossRefSearch = crossRefSearch;
247ae1c323bSVladimir Kotal         this.contextPath = contextPath;
248ae1c323bSVladimir Kotal         this.guiSearch = guiSearch;
249ae1c323bSVladimir Kotal         this.noRedirect = noRedirect;
250ae1c323bSVladimir Kotal     }
251ae1c323bSVladimir Kotal 
getDataRoot()252ae1c323bSVladimir Kotal     public File getDataRoot() {
253ae1c323bSVladimir Kotal         return dataRoot;
254ae1c323bSVladimir Kotal     }
255ae1c323bSVladimir Kotal 
getSourceRoot()256ae1c323bSVladimir Kotal     public File getSourceRoot() {
257ae1c323bSVladimir Kotal         return sourceRoot;
258ae1c323bSVladimir Kotal     }
259ae1c323bSVladimir Kotal 
getDesc()260ae1c323bSVladimir Kotal     public EftarFileReader getDesc() {
261ae1c323bSVladimir Kotal         return desc;
262ae1c323bSVladimir Kotal     }
263ae1c323bSVladimir Kotal 
getBuilder()264ae1c323bSVladimir Kotal     public QueryBuilder getBuilder() {
265ae1c323bSVladimir Kotal         return builder;
266ae1c323bSVladimir Kotal     }
267ae1c323bSVladimir Kotal 
getContextPath()268ae1c323bSVladimir Kotal     public String getContextPath() {
269ae1c323bSVladimir Kotal         return contextPath;
270ae1c323bSVladimir Kotal     }
271ae1c323bSVladimir Kotal 
setRedirect(String redirect)272ae1c323bSVladimir Kotal     public void setRedirect(String redirect) {
273ae1c323bSVladimir Kotal         this.redirect = redirect;
274ae1c323bSVladimir Kotal     }
275ae1c323bSVladimir Kotal 
getRedirect()276ae1c323bSVladimir Kotal     public String getRedirect() {
277ae1c323bSVladimir Kotal         return redirect;
278ae1c323bSVladimir Kotal     }
279ae1c323bSVladimir Kotal 
getErrorMsg()280ae1c323bSVladimir Kotal     public String getErrorMsg() {
281ae1c323bSVladimir Kotal         return errorMsg;
282ae1c323bSVladimir Kotal     }
283ae1c323bSVladimir Kotal 
setErrorMsg(String errorMsg)284ae1c323bSVladimir Kotal     public void setErrorMsg(String errorMsg) {
285ae1c323bSVladimir Kotal         this.errorMsg = errorMsg;
286ae1c323bSVladimir Kotal     }
287ae1c323bSVladimir Kotal 
getSearcher()288ae1c323bSVladimir Kotal     public IndexSearcher getSearcher() {
289ae1c323bSVladimir Kotal         return searcher;
290ae1c323bSVladimir Kotal     }
291ae1c323bSVladimir Kotal 
getHits()292ae1c323bSVladimir Kotal     public ScoreDoc[] getHits() {
293ae1c323bSVladimir Kotal         return hits;
294ae1c323bSVladimir Kotal     }
295ae1c323bSVladimir Kotal 
getQuery()296ae1c323bSVladimir Kotal     public Query getQuery() {
297ae1c323bSVladimir Kotal         return query;
298ae1c323bSVladimir Kotal     }
299ae1c323bSVladimir Kotal 
getTotalHits()300ae1c323bSVladimir Kotal     public long getTotalHits() {
301ae1c323bSVladimir Kotal         return totalHits;
302ae1c323bSVladimir Kotal     }
303ae1c323bSVladimir Kotal 
getProjects()304ae1c323bSVladimir Kotal     public SortedSet<String> getProjects() {
305ae1c323bSVladimir Kotal         return projects;
306ae1c323bSVladimir Kotal     }
307ae1c323bSVladimir Kotal 
getSourceContext()308ae1c323bSVladimir Kotal     public Context getSourceContext() {
309ae1c323bSVladimir Kotal         return sourceContext;
310ae1c323bSVladimir Kotal     }
311ae1c323bSVladimir Kotal 
getMaxItems()312ae1c323bSVladimir Kotal     public int getMaxItems() {
313ae1c323bSVladimir Kotal         return maxItems;
314ae1c323bSVladimir Kotal     }
315ae1c323bSVladimir Kotal 
getOrder()316ae1c323bSVladimir Kotal     public SortOrder getOrder() {
317ae1c323bSVladimir Kotal         return order;
318ae1c323bSVladimir Kotal     }
319ae1c323bSVladimir Kotal 
getStart()320ae1c323bSVladimir Kotal     public int getStart() {
321ae1c323bSVladimir Kotal         return start;
322ae1c323bSVladimir Kotal     }
323ae1c323bSVladimir Kotal 
getSummarizer()324ae1c323bSVladimir Kotal     public Summarizer getSummarizer() {
325ae1c323bSVladimir Kotal         return summarizer;
326ae1c323bSVladimir Kotal     }
327ae1c323bSVladimir Kotal 
getHistoryContext()328ae1c323bSVladimir Kotal     public HistoryContext getHistoryContext() {
329ae1c323bSVladimir Kotal         return historyContext;
330ae1c323bSVladimir Kotal     }
331ae1c323bSVladimir Kotal 
332b5840353SAdam Hornáček     /**
333b5840353SAdam Hornáček      * User readable description for file types. Only those listed in
334b5840353SAdam Hornáček      * fileTypeDescription will be shown to the user.
335b5840353SAdam Hornáček      *
336b5840353SAdam Hornáček      * Returns a set of file type descriptions to be used for a search form.
337b5840353SAdam Hornáček      *
338b5840353SAdam Hornáček      * @return Set of tuples with file type and description.
339b5840353SAdam Hornáček      */
getFileTypeDescriptions()340b5840353SAdam Hornáček     public static Set<Map.Entry<String, String>> getFileTypeDescriptions() {
341b5840353SAdam Hornáček         return AnalyzerGuru.getfileTypeDescriptions().entrySet();
342b5840353SAdam Hornáček     }
343b5840353SAdam Hornáček 
344b5840353SAdam Hornáček     /**
345b5840353SAdam Hornáček      * Create the searcher to use w.r.t. currently set parameters and the given
346b5840353SAdam Hornáček      * projects. Does not produce any {@link #redirect} link. It also does
347b5840353SAdam Hornáček      * nothing if {@link #redirect} or {@link #errorMsg} have a
348b5840353SAdam Hornáček      * none-{@code null} value.
349b5840353SAdam Hornáček      * <p>
350b5840353SAdam Hornáček      * Parameters which should be populated/set at this time:
351b5840353SAdam Hornáček      * <ul>
352b5840353SAdam Hornáček      * <li>{@link #builder}</li> <li>{@link #dataRoot}</li>
353b5840353SAdam Hornáček      * <li>{@link #order} (falls back to relevance if unset)</li>
354ae1c323bSVladimir Kotal      * </ul>
355ae1c323bSVladimir Kotal      * Populates/sets:
356ae1c323bSVladimir Kotal      * <ul>
357b5840353SAdam Hornáček      * <li>{@link #query}</li> <li>{@link #searcher}</li> <li>{@link #sort}</li>
358b5840353SAdam Hornáček      * <li>{@link #projects}</li> <li>{@link #errorMsg} if an error occurs</li>
359b5840353SAdam Hornáček      * </ul>
360b5840353SAdam Hornáček      *
361b5840353SAdam Hornáček      * @param projects project names. If empty, a no-project setup
362b5840353SAdam Hornáček      * is assumed (i.e. DATA_ROOT/index will be used instead of possible
363b5840353SAdam Hornáček      * multiple DATA_ROOT/$project/index). If the set contains projects
364b5840353SAdam Hornáček      * not known in the configuration or projects not yet indexed,
365b5840353SAdam Hornáček      * an error will be returned in {@link #errorMsg}.
366b5840353SAdam Hornáček      * @return this instance
367b5840353SAdam Hornáček      */
prepareExec(SortedSet<String> projects)368b5840353SAdam Hornáček     public SearchHelper prepareExec(SortedSet<String> projects) {
369b5840353SAdam Hornáček         if (redirect != null || errorMsg != null) {
370b5840353SAdam Hornáček             return this;
371b5840353SAdam Hornáček         }
372b5840353SAdam Hornáček 
373497eb8c9SChris Fraire         settingsHelper = null;
374b5840353SAdam Hornáček         // the Query created by the QueryBuilder
375b5840353SAdam Hornáček         try {
376b5840353SAdam Hornáček             indexDir = new File(dataRoot, IndexDatabase.INDEX_DIR);
377b5840353SAdam Hornáček             query = builder.build();
378b5840353SAdam Hornáček             if (projects == null) {
379b5840353SAdam Hornáček                 errorMsg = "No project selected!";
380b5840353SAdam Hornáček                 return this;
381b5840353SAdam Hornáček             }
382b5840353SAdam Hornáček             this.projects = projects;
383b5840353SAdam Hornáček             if (projects.isEmpty()) {
384b5840353SAdam Hornáček                 // no project setup
385b5840353SAdam Hornáček                 FSDirectory dir = FSDirectory.open(indexDir.toPath());
386b5840353SAdam Hornáček                 reader = DirectoryReader.open(dir);
387b5840353SAdam Hornáček                 searcher = new IndexSearcher(reader);
388b5840353SAdam Hornáček                 closeOnDestroy = true;
389b5840353SAdam Hornáček             } else {
390b5840353SAdam Hornáček                 // Check list of project names first to make sure all of them
391b5840353SAdam Hornáček                 // are valid and indexed.
392b5840353SAdam Hornáček                 closeOnDestroy = false;
393b5840353SAdam Hornáček                 Set<String> invalidProjects = projects.stream().
394b5840353SAdam Hornáček                     filter(proj -> (Project.getByName(proj) == null)).
395b5840353SAdam Hornáček                     collect(Collectors.toSet());
396311d83f9SVladimir Kotal                 if (!invalidProjects.isEmpty()) {
397b5840353SAdam Hornáček                     errorMsg = "Project list contains invalid projects: " +
398b5840353SAdam Hornáček                         String.join(", ", invalidProjects);
399b5840353SAdam Hornáček                     return this;
400b5840353SAdam Hornáček                 }
401b5840353SAdam Hornáček                 Set<Project> notIndexedProjects =
402b5840353SAdam Hornáček                     projects.stream().
403c6f0939bSAdam Hornacek                     map(Project::getByName).
404b5840353SAdam Hornáček                     filter(proj -> !proj.isIndexed()).
405b5840353SAdam Hornáček                     collect(Collectors.toSet());
406311d83f9SVladimir Kotal                 if (!notIndexedProjects.isEmpty()) {
407b5840353SAdam Hornáček                     errorMsg = "Some of the projects to be searched are not indexed yet: " +
408b5840353SAdam Hornáček                         String.join(", ", notIndexedProjects.stream().
409c6f0939bSAdam Hornacek                         map(Project::getName).
410b5840353SAdam Hornáček                         collect(Collectors.toSet()));
411b5840353SAdam Hornáček                     return this;
412b5840353SAdam Hornáček                 }
413b5840353SAdam Hornáček 
414b5840353SAdam Hornáček                 // We use MultiReader even for single project. This should
415b5840353SAdam Hornáček                 // not matter given that MultiReader is just a cheap wrapper
416b5840353SAdam Hornáček                 // around set of IndexReader objects.
41738625bbaSVladimir Kotal                 reader = RuntimeEnvironment.getInstance().getMultiReader(projects, searcherList);
418b5840353SAdam Hornáček                 if (reader != null) {
419b5840353SAdam Hornáček                     searcher = new IndexSearcher(reader);
420b5840353SAdam Hornáček                 } else {
42138625bbaSVladimir Kotal                     errorMsg = "Failed to initialize search. Check the index";
4224095db0fSVladimir Kotal                     if (!projects.isEmpty()) {
423b1860bc8SVladimir Kotal                         errorMsg += " for projects: " + String.join(", ", projects);
42438625bbaSVladimir Kotal                     }
425b5840353SAdam Hornáček                     return this;
426b5840353SAdam Hornáček                 }
427b5840353SAdam Hornáček             }
428b5840353SAdam Hornáček 
429b5840353SAdam Hornáček             // TODO check if below is somehow reusing sessions so we don't
430b5840353SAdam Hornáček             // requery again and again, I guess 2min timeout sessions could be
431b5840353SAdam Hornáček             // useful, since you click on the next page within 2mins, if not,
432b5840353SAdam Hornáček             // then wait ;)
433b5840353SAdam Hornáček             // Most probably they are not reused. SearcherLifetimeManager might help here.
434b5840353SAdam Hornáček             switch (order) {
435b5840353SAdam Hornáček                 case LASTMODIFIED:
436b5840353SAdam Hornáček                     sort = new Sort(new SortField(QueryBuilder.DATE, SortField.Type.STRING, true));
437b5840353SAdam Hornáček                     break;
438b5840353SAdam Hornáček                 case BY_PATH:
439b5840353SAdam Hornáček                     sort = new Sort(new SortField(QueryBuilder.FULLPATH, SortField.Type.STRING));
440b5840353SAdam Hornáček                     break;
441b5840353SAdam Hornáček                 default:
442b5840353SAdam Hornáček                     sort = Sort.RELEVANCE;
443b5840353SAdam Hornáček                     break;
444b5840353SAdam Hornáček             }
445b5840353SAdam Hornáček             checker = new DirectSpellChecker();
446b5840353SAdam Hornáček         } catch (ParseException e) {
447b5840353SAdam Hornáček             errorMsg = PARSE_ERROR_MSG + e.getMessage();
448b5840353SAdam Hornáček         } catch (FileNotFoundException e) {
449bc170285SVladimir Kotal             errorMsg = "Index database not found. Check the index";
4504095db0fSVladimir Kotal             if (!projects.isEmpty()) {
451b1860bc8SVladimir Kotal                 errorMsg += " for projects: " + String.join(", ", projects);
45238625bbaSVladimir Kotal             }
453b1860bc8SVladimir Kotal             errorMsg += "; " + e.getMessage();
454b5840353SAdam Hornáček         } catch (IOException e) {
455b5840353SAdam Hornáček             errorMsg = e.getMessage();
456b5840353SAdam Hornáček         }
457b5840353SAdam Hornáček         return this;
458b5840353SAdam Hornáček     }
459b5840353SAdam Hornáček 
460b5840353SAdam Hornáček     /**
461b5840353SAdam Hornáček      * Calls {@link #prepareExec(java.util.SortedSet)} with a single-element
462b5840353SAdam Hornáček      * set for {@code project}.
463b5840353SAdam Hornáček      * @param project a defined instance
464b5840353SAdam Hornáček      * @return this instance
465b5840353SAdam Hornáček      */
prepareExec(Project project)466b5840353SAdam Hornáček     public SearchHelper prepareExec(Project project) {
467b5840353SAdam Hornáček         SortedSet<String> oneProject = new TreeSet<>();
468b5840353SAdam Hornáček         oneProject.add(project.getName());
469b5840353SAdam Hornáček         return prepareExec(oneProject);
470b5840353SAdam Hornáček     }
471b5840353SAdam Hornáček 
472b5840353SAdam Hornáček     /**
473b5840353SAdam Hornáček      * Start the search prepared by {@link #prepareExec(SortedSet)}. It does
474b5840353SAdam Hornáček      * nothing if {@link #redirect} or {@link #errorMsg} have a
475b5840353SAdam Hornáček      * none-{@code null} value.
476b5840353SAdam Hornáček      * <p>
477b5840353SAdam Hornáček      * Parameters which should be populated/set at this time: <ul> <li>all
478b5840353SAdam Hornáček      * fields required for and populated by
479b5840353SAdam Hornáček      * {@link #prepareExec(SortedSet)})</li> <li>{@link #start} (default:
480b5840353SAdam Hornáček      * 0)</li> <li>{@link #maxItems} (default: 0)</li>
481ae1c323bSVladimir Kotal      * <li>{@link #crossRefSearch} (default: false)</li> </ul> Populates/sets:
482b5840353SAdam Hornáček      * <ul> <li>{@link #hits} (see {@link TopFieldDocs#scoreDocs})</li>
483b5840353SAdam Hornáček      * <li>{@link #totalHits} (see {@link TopFieldDocs#totalHits})</li>
484b5840353SAdam Hornáček      * <li>{@link #contextPath}</li> <li>{@link #errorMsg} if an error
485b5840353SAdam Hornáček      * occurs</li> <li>{@link #redirect} if certain conditions are met</li>
486b5840353SAdam Hornáček      * </ul>
487b5840353SAdam Hornáček      *
488b5840353SAdam Hornáček      * @return this instance
489b5840353SAdam Hornáček      */
executeQuery()490b5840353SAdam Hornáček     public SearchHelper executeQuery() {
491b5840353SAdam Hornáček         if (redirect != null || errorMsg != null) {
492b5840353SAdam Hornáček             return this;
493b5840353SAdam Hornáček         }
494b5840353SAdam Hornáček         try {
495b5840353SAdam Hornáček             TopFieldDocs fdocs = searcher.search(query, start + maxItems, sort);
4964cf88309SLubos Kosco             totalHits = fdocs.totalHits.value;
497b5840353SAdam Hornáček             hits = fdocs.scoreDocs;
498b5840353SAdam Hornáček 
499eeb95924SChris Fraire             /*
500eeb95924SChris Fraire              * Determine if possibly a single-result redirect to xref is
501eeb95924SChris Fraire              * eligible and applicable. If history query is active, then nope.
502eeb95924SChris Fraire              */
50399167638SChris Fraire             if (!noRedirect && hits != null && hits.length == 1 && builder.getHist() == null) {
504eeb95924SChris Fraire                 int docID = hits[0].doc;
505ae1c323bSVladimir Kotal                 if (crossRefSearch && query instanceof TermQuery && builder.getDefs() != null) {
506eeb95924SChris Fraire                     maybeRedirectToDefinition(docID, (TermQuery) query);
507ae1c323bSVladimir Kotal                 } else if (guiSearch) {
508974067deSChris Fraire                     if (builder.isPathSearch()) {
509974067deSChris Fraire                         redirectToFile(docID);
510974067deSChris Fraire                     } else {
511eeb95924SChris Fraire                         maybeRedirectToMatchOffset(docID, builder.getContextFields());
512b5840353SAdam Hornáček                     }
513b5840353SAdam Hornáček                 }
514974067deSChris Fraire             }
515b5840353SAdam Hornáček         } catch (IOException | ClassNotFoundException e) {
516b5840353SAdam Hornáček             errorMsg = e.getMessage();
517b5840353SAdam Hornáček         }
518b5840353SAdam Hornáček         return this;
519b5840353SAdam Hornáček     }
520eeb95924SChris Fraire 
maybeRedirectToDefinition(int docID, TermQuery termQuery)521eeb95924SChris Fraire     private void maybeRedirectToDefinition(int docID, TermQuery termQuery)
522eeb95924SChris Fraire             throws IOException, ClassNotFoundException {
523eeb95924SChris Fraire         // Bug #3900: Check if this is a search for a single term, and that
524eeb95924SChris Fraire         // term is a definition. If that's the case, and we only have one match,
525eeb95924SChris Fraire         // we'll generate a direct link instead of a listing.
526eeb95924SChris Fraire         //
527eeb95924SChris Fraire         // Attempt to create a direct link to the definition if we search for
528eeb95924SChris Fraire         // one single definition term AND we have exactly one match AND there
529eeb95924SChris Fraire         // is only one definition of that symbol in the document that matches.
530eeb95924SChris Fraire         Document doc = searcher.doc(docID);
531eeb95924SChris Fraire         IndexableField tagsField = doc.getField(QueryBuilder.TAGS);
532eeb95924SChris Fraire         if (tagsField != null) {
533eeb95924SChris Fraire             byte[] rawTags = tagsField.binaryValue().bytes;
534eeb95924SChris Fraire             Definitions tags = Definitions.deserialize(rawTags);
535eeb95924SChris Fraire             String symbol = termQuery.getTerm().text();
536eeb95924SChris Fraire             if (tags.occurrences(symbol) == 1) {
537*d6df19e1SAdam Hornacek                 String anchor = Util.uriEncode(symbol);
538eeb95924SChris Fraire                 redirect = contextPath + Prefix.XREF_P
539*d6df19e1SAdam Hornacek                         + Util.uriEncodePath(doc.get(QueryBuilder.PATH))
540eeb95924SChris Fraire                         + '?' + QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ + anchor
541eeb95924SChris Fraire                         + '#' + anchor;
542eeb95924SChris Fraire             }
543eeb95924SChris Fraire         }
544eeb95924SChris Fraire     }
545eeb95924SChris Fraire 
maybeRedirectToMatchOffset(int docID, List<String> contextFields)546eeb95924SChris Fraire     private void maybeRedirectToMatchOffset(int docID, List<String> contextFields)
547eeb95924SChris Fraire             throws IOException {
548eeb95924SChris Fraire         /*
549eeb95924SChris Fraire          * Only PLAIN files might redirect to a file offset, since an offset
550eeb95924SChris Fraire          * must be subsequently converted to a line number and that is tractable
551eeb95924SChris Fraire          * only from plain text.
552eeb95924SChris Fraire          */
553eeb95924SChris Fraire         Document doc = searcher.doc(docID);
554eeb95924SChris Fraire         String genre = doc.get(QueryBuilder.T);
555eeb95924SChris Fraire         if (!AbstractAnalyzer.Genre.PLAIN.typeName().equals(genre)) {
556eeb95924SChris Fraire             return;
557eeb95924SChris Fraire         }
558eeb95924SChris Fraire 
559eeb95924SChris Fraire         List<LeafReaderContext> leaves = reader.leaves();
560eeb95924SChris Fraire         int subIndex = ReaderUtil.subIndex(docID, leaves);
561eeb95924SChris Fraire         LeafReaderContext leaf = leaves.get(subIndex);
562eeb95924SChris Fraire 
563eeb95924SChris Fraire         Query rewritten = query.rewrite(reader);
564eeb95924SChris Fraire         Weight weight = rewritten.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1);
56501dfb76cSChris Fraire         Matches matches = weight.matches(leaf, docID - leaf.docBase); // Adjust docID
566043289deSChris Fraire         if (matches != null && matches != MatchesUtils.MATCH_WITH_NO_TERMS) {
567eeb95924SChris Fraire             int matchCount = 0;
568eeb95924SChris Fraire             int offset = -1;
569eeb95924SChris Fraire             for (String field : contextFields) {
570eeb95924SChris Fraire                 MatchesIterator matchesIterator = matches.getMatches(field);
571eeb95924SChris Fraire                 while (matchesIterator.next()) {
572eeb95924SChris Fraire                     if (matchesIterator.startOffset() >= 0) {
573eeb95924SChris Fraire                         // Abort if there is more than a single match offset.
574eeb95924SChris Fraire                         if (++matchCount > 1) {
575eeb95924SChris Fraire                             return;
576eeb95924SChris Fraire                         }
577eeb95924SChris Fraire                         offset = matchesIterator.startOffset();
578eeb95924SChris Fraire                     }
579eeb95924SChris Fraire                 }
580eeb95924SChris Fraire             }
581eeb95924SChris Fraire             if (offset >= 0) {
582eeb95924SChris Fraire                 redirect = contextPath + Prefix.XREF_P
583*d6df19e1SAdam Hornacek                         + Util.uriEncodePath(doc.get(QueryBuilder.PATH))
584eeb95924SChris Fraire                         + '?' + QueryParameters.MATCH_OFFSET_PARAM_EQ + offset;
585eeb95924SChris Fraire             }
586eeb95924SChris Fraire         }
587eeb95924SChris Fraire     }
588eeb95924SChris Fraire 
redirectToFile(int docID)589974067deSChris Fraire     private void redirectToFile(int docID) throws IOException {
590974067deSChris Fraire         Document doc = searcher.doc(docID);
591*d6df19e1SAdam Hornacek         redirect = contextPath + Prefix.XREF_P + Util.uriEncodePath(doc.get(QueryBuilder.PATH));
592974067deSChris Fraire     }
593974067deSChris Fraire 
getSuggestion(Term term, IndexReader ir, List<String> result)594b5840353SAdam Hornáček     private void getSuggestion(Term term, IndexReader ir,
595b5840353SAdam Hornáček             List<String> result) throws IOException {
596b5840353SAdam Hornáček         if (term == null) {
597b5840353SAdam Hornáček             return;
598b5840353SAdam Hornáček         }
599ae1c323bSVladimir Kotal         String[] toks = TAB_SPACE.split(term.text(), 0);
600b5840353SAdam Hornáček         for (String tok : toks) {
601b5840353SAdam Hornáček             //TODO below seems to be case insensitive ... for refs/defs this is bad
602b5840353SAdam Hornáček             SuggestWord[] words = checker.suggestSimilar(new Term(term.field(), tok),
603b5840353SAdam Hornáček                 SPELLCHECK_SUGGEST_WORD_COUNT, ir, SuggestMode.SUGGEST_ALWAYS);
604b5840353SAdam Hornáček             for (SuggestWord w : words) {
605b5840353SAdam Hornáček                 result.add(w.string);
606b5840353SAdam Hornáček             }
607b5840353SAdam Hornáček         }
608b5840353SAdam Hornáček     }
609b5840353SAdam Hornáček 
610b5840353SAdam Hornáček     /**
611b5840353SAdam Hornáček      * If a search did not return a hit, one may use this method to obtain
612b5840353SAdam Hornáček      * suggestions for a new search.
613b5840353SAdam Hornáček      *
614b5840353SAdam Hornáček      * <p>
615b5840353SAdam Hornáček      * Parameters which should be populated/set at this time: <ul>
616b5840353SAdam Hornáček      * <li>{@link #projects}</li> <li>{@link #dataRoot}</li>
617b5840353SAdam Hornáček      * <li>{@link #builder}</li> </ul>
618b5840353SAdam Hornáček      *
619b5840353SAdam Hornáček      * @return a possible empty list of suggestions.
620b5840353SAdam Hornáček      */
getSuggestions()621b5840353SAdam Hornáček     public List<Suggestion> getSuggestions() {
622b5840353SAdam Hornáček         if (projects == null) {
623b5840353SAdam Hornáček             return new ArrayList<>(0);
624b5840353SAdam Hornáček         }
625d1e826faSAdam Hornáček         String[] name;
626b5840353SAdam Hornáček         if (projects.isEmpty()) {
627b5840353SAdam Hornáček             name = new String[]{"/"};
628b5840353SAdam Hornáček         } else if (projects.size() == 1) {
629b5840353SAdam Hornáček             name = new String[]{projects.first()};
630b5840353SAdam Hornáček         } else {
631b5840353SAdam Hornáček             name = new String[projects.size()];
632b5840353SAdam Hornáček             int ii = 0;
633b5840353SAdam Hornáček             for (String proj : projects) {
634b5840353SAdam Hornáček                 name[ii++] = proj;
635b5840353SAdam Hornáček             }
636b5840353SAdam Hornáček         }
637b5840353SAdam Hornáček         List<Suggestion> res = new ArrayList<>();
638b5840353SAdam Hornáček         List<String> dummy = new ArrayList<>();
639b5840353SAdam Hornáček         FSDirectory dir;
640b5840353SAdam Hornáček         IndexReader ir = null;
641b5840353SAdam Hornáček         Term t;
642b5840353SAdam Hornáček         for (String proj : name) {
64368076d3cSVladimir Kotal             Suggestion suggestion = new Suggestion(proj);
644b5840353SAdam Hornáček             try {
645b5840353SAdam Hornáček                 if (!closeOnDestroy) {
646b5840353SAdam Hornáček                     SuperIndexSearcher searcher = RuntimeEnvironment.getInstance().getIndexSearcher(proj);
647b5840353SAdam Hornáček                     searcherList.add(searcher);
648b5840353SAdam Hornáček                     ir = searcher.getIndexReader();
649b5840353SAdam Hornáček                 } else {
650b5840353SAdam Hornáček                     dir = FSDirectory.open(new File(indexDir, proj).toPath());
651b5840353SAdam Hornáček                     ir = DirectoryReader.open(dir);
652b5840353SAdam Hornáček                 }
653b5840353SAdam Hornáček                 if (builder.getFreetext() != null
654b5840353SAdam Hornáček                         && !builder.getFreetext().isEmpty()) {
655b5840353SAdam Hornáček                     t = new Term(QueryBuilder.FULL, builder.getFreetext());
656b5840353SAdam Hornáček                     getSuggestion(t, ir, dummy);
65768076d3cSVladimir Kotal                     suggestion.setFreetext(dummy.toArray(new String[0]));
658b5840353SAdam Hornáček                     dummy.clear();
659b5840353SAdam Hornáček                 }
660b5840353SAdam Hornáček                 if (builder.getRefs() != null && !builder.getRefs().isEmpty()) {
661b5840353SAdam Hornáček                     t = new Term(QueryBuilder.REFS, builder.getRefs());
662b5840353SAdam Hornáček                     getSuggestion(t, ir, dummy);
66368076d3cSVladimir Kotal                     suggestion.setRefs(dummy.toArray(new String[0]));
664b5840353SAdam Hornáček                     dummy.clear();
665b5840353SAdam Hornáček                 }
666b5840353SAdam Hornáček                 if (builder.getDefs() != null && !builder.getDefs().isEmpty()) {
667b5840353SAdam Hornáček                     t = new Term(QueryBuilder.DEFS, builder.getDefs());
668b5840353SAdam Hornáček                     getSuggestion(t, ir, dummy);
66968076d3cSVladimir Kotal                     suggestion.setDefs(dummy.toArray(new String[0]));
670b5840353SAdam Hornáček                     dummy.clear();
671b5840353SAdam Hornáček                 }
672b5840353SAdam Hornáček                 //TODO suggest also for path and history?
67368076d3cSVladimir Kotal                 if (suggestion.isUsable()) {
67468076d3cSVladimir Kotal                     res.add(suggestion);
675b5840353SAdam Hornáček                 }
676b5840353SAdam Hornáček             } catch (IOException e) {
67768076d3cSVladimir Kotal                 LOGGER.log(Level.WARNING,
67868076d3cSVladimir Kotal                         String.format("Got exception while getting spelling suggestions for project %s:", proj), e);
679b5840353SAdam Hornáček             } finally {
680b5840353SAdam Hornáček                 if (ir != null && closeOnDestroy) {
681b5840353SAdam Hornáček                     try {
682b5840353SAdam Hornáček                         ir.close();
683b5840353SAdam Hornáček                     } catch (IOException ex) {
684b5840353SAdam Hornáček                         LOGGER.log(Level.WARNING, "Got exception while "
685b5840353SAdam Hornáček                                 + "getting spelling suggestions: ", ex);
686b5840353SAdam Hornáček                     }
687b5840353SAdam Hornáček                 }
688b5840353SAdam Hornáček             }
689b5840353SAdam Hornáček         }
690b5840353SAdam Hornáček         return res;
691b5840353SAdam Hornáček     }
692b5840353SAdam Hornáček 
693b5840353SAdam Hornáček     /**
694b5840353SAdam Hornáček      * Prepare the fields to support printing a full blown summary. Does nothing
695b5840353SAdam Hornáček      * if {@link #redirect} or {@link #errorMsg} have a none-{@code null} value.
696b5840353SAdam Hornáček      *
697b5840353SAdam Hornáček      * <p>
698b5840353SAdam Hornáček      * Parameters which should be populated/set at this time: <ul>
699b5840353SAdam Hornáček      * <li>{@link #query}</li> <li>{@link #builder}</li> </ul> Populates/sets:
700b5840353SAdam Hornáček      * Otherwise the following fields are set (includes {@code null}): <ul>
701b5840353SAdam Hornáček      * <li>{@link #sourceContext}</li> <li>{@link #summarizer}</li>
702b5840353SAdam Hornáček      * <li>{@link #historyContext}</li> </ul>
703b5840353SAdam Hornáček      *
704b5840353SAdam Hornáček      * @return this instance.
705b5840353SAdam Hornáček      */
prepareSummary()706b5840353SAdam Hornáček     public SearchHelper prepareSummary() {
707b5840353SAdam Hornáček         if (redirect != null || errorMsg != null) {
708b5840353SAdam Hornáček             return this;
709b5840353SAdam Hornáček         }
710b5840353SAdam Hornáček         try {
711b5840353SAdam Hornáček             sourceContext = new Context(query, builder);
712b5840353SAdam Hornáček             summarizer = new Summarizer(query, new CompatibleAnalyser());
713b5840353SAdam Hornáček         } catch (Exception e) {
714b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "Summarizer: {0}", e.getMessage());
715b5840353SAdam Hornáček         }
716b5840353SAdam Hornáček         try {
717b5840353SAdam Hornáček             historyContext = new HistoryContext(query);
718b5840353SAdam Hornáček         } catch (Exception e) {
719b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "HistoryContext: {0}", e.getMessage());
720b5840353SAdam Hornáček         }
721b5840353SAdam Hornáček         return this;
722b5840353SAdam Hornáček     }
723b5840353SAdam Hornáček 
724b5840353SAdam Hornáček     /**
725b5840353SAdam Hornáček      * Free any resources associated with this helper (that includes closing the
726b5840353SAdam Hornáček      * used {@link #searcher} in case of no-project setup).
727b5840353SAdam Hornáček      */
destroy()728b5840353SAdam Hornáček     public void destroy() {
729b5840353SAdam Hornáček         if (searcher != null && closeOnDestroy) {
730b5840353SAdam Hornáček             IOUtils.close(searcher.getIndexReader());
731b5840353SAdam Hornáček         }
732b5840353SAdam Hornáček 
733b5840353SAdam Hornáček         for (SuperIndexSearcher is : searcherList) {
734b5840353SAdam Hornáček             try {
735b5840353SAdam Hornáček                 is.getSearcherManager().release(is);
736b5840353SAdam Hornáček             } catch (IOException ex) {
737b5840353SAdam Hornáček                 LOGGER.log(Level.WARNING, "cannot release indexSearcher", ex);
738b5840353SAdam Hornáček             }
739b5840353SAdam Hornáček         }
740b5840353SAdam Hornáček     }
741b5840353SAdam Hornáček 
742b5840353SAdam Hornáček     /**
743b5840353SAdam Hornáček      * Searches for a document for a single file from the index.
744b5840353SAdam Hornáček      * @param file the file whose definitions to find
745b5840353SAdam Hornáček      * @return {@link ScoreDoc#doc} or -1 if it could not be found
746b5840353SAdam Hornáček      * @throws IOException if an error happens when accessing the index
747b5840353SAdam Hornáček      * @throws ParseException if an error happens when building the Lucene query
748b5840353SAdam Hornáček      */
searchSingle(File file)749b5840353SAdam Hornáček     public int searchSingle(File file) throws IOException,
750b5840353SAdam Hornáček             ParseException {
751b5840353SAdam Hornáček 
752b5840353SAdam Hornáček         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
753b5840353SAdam Hornáček         String path;
754b5840353SAdam Hornáček         try {
755b5840353SAdam Hornáček             path = env.getPathRelativeToSourceRoot(file);
756b5840353SAdam Hornáček         } catch (ForbiddenSymlinkException e) {
757b5840353SAdam Hornáček             LOGGER.log(Level.FINER, e.getMessage());
758b5840353SAdam Hornáček             return -1;
759b5840353SAdam Hornáček         }
760b5840353SAdam Hornáček         //sanitize windows path delimiters
761b5840353SAdam Hornáček         //in order not to conflict with Lucene escape character
762b5840353SAdam Hornáček         path = path.replace("\\", "/");
763b5840353SAdam Hornáček 
764b5840353SAdam Hornáček         QueryBuilder singleBuilder = new QueryBuilder();
765b5840353SAdam Hornáček         if (builder != null) {
766b5840353SAdam Hornáček             singleBuilder.reset(builder);
767b5840353SAdam Hornáček         }
768b5840353SAdam Hornáček         query = singleBuilder.setPath(path).build();
769b5840353SAdam Hornáček 
770b5840353SAdam Hornáček         TopDocs top = searcher.search(query, 1);
7714cf88309SLubos Kosco         if (top.totalHits.value == 0) {
772b5840353SAdam Hornáček             return -1;
773b5840353SAdam Hornáček         }
774b5840353SAdam Hornáček 
775b5840353SAdam Hornáček         int docID = top.scoreDocs[0].doc;
776b5840353SAdam Hornáček         Document doc = searcher.doc(docID);
777b5840353SAdam Hornáček 
778b5840353SAdam Hornáček         String foundPath = doc.get(QueryBuilder.PATH);
779b5840353SAdam Hornáček         // Only use the result if PATH matches exactly.
780b5840353SAdam Hornáček         if (!path.equals(foundPath)) {
781b5840353SAdam Hornáček             return -1;
782b5840353SAdam Hornáček         }
783b5840353SAdam Hornáček 
784b5840353SAdam Hornáček         return docID;
785b5840353SAdam Hornáček     }
786b5840353SAdam Hornáček 
787b5840353SAdam Hornáček     /**
788497eb8c9SChris Fraire      * Gets the persisted tabSize via {@link SettingsHelper} for the active
789497eb8c9SChris Fraire      * reader.
790b5840353SAdam Hornáček      * @param proj a defined instance or {@code null} if no project is active
791b5840353SAdam Hornáček      * @return tabSize
792b5840353SAdam Hornáček      * @throws IOException if an I/O error occurs querying the active reader
793b5840353SAdam Hornáček      */
getTabSize(Project proj)794b5840353SAdam Hornáček     public int getTabSize(Project proj) throws IOException {
795497eb8c9SChris Fraire         ensureSettingsHelper();
796497eb8c9SChris Fraire         return settingsHelper.getTabSize(proj);
797b5840353SAdam Hornáček     }
798b5840353SAdam Hornáček 
799b5840353SAdam Hornáček     /**
8007d004396SChris Fraire      * Determines if there is a prime equivalent to {@code relativePath}
8017d004396SChris Fraire      * according to indexed symlinks and translate (or not) accordingly.
8027d004396SChris Fraire      * @param project the project name or empty string if projects are not used
8037d004396SChris Fraire      * @param relativePath an OpenGrok-style (i.e. starting with a file
8047d004396SChris Fraire      *                     separator) relative path
8057d004396SChris Fraire      * @return a prime relative path or just {@code relativePath} if no prime
8067d004396SChris Fraire      * is matched
8077d004396SChris Fraire      */
getPrimeRelativePath(String project, String relativePath)8087d004396SChris Fraire     public String getPrimeRelativePath(String project, String relativePath)
8097d004396SChris Fraire             throws IOException, ForbiddenSymlinkException {
8107d004396SChris Fraire 
8117d004396SChris Fraire         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
8127d004396SChris Fraire         String sourceRoot = env.getSourceRootPath();
8137d004396SChris Fraire         if (sourceRoot == null) {
8147d004396SChris Fraire             throw new IllegalStateException("sourceRoot is not defined");
8157d004396SChris Fraire         }
8167d004396SChris Fraire         File absolute = new File(sourceRoot + relativePath);
8177d004396SChris Fraire 
818497eb8c9SChris Fraire         ensureSettingsHelper();
819497eb8c9SChris Fraire         settingsHelper.getSettings(project);
820497eb8c9SChris Fraire         Map<String, IndexedSymlink> indexedSymlinks = settingsHelper.getSymlinks(project);
821497eb8c9SChris Fraire         if (indexedSymlinks != null) {
8227d004396SChris Fraire             String canonical = absolute.getCanonicalFile().getPath();
8237d004396SChris Fraire             for (IndexedSymlink entry : indexedSymlinks.values()) {
8247d004396SChris Fraire                 if (canonical.equals(entry.getCanonical())) {
8257d004396SChris Fraire                     if (absolute.getPath().equals(entry.getAbsolute())) {
8267d004396SChris Fraire                         return relativePath;
8277d004396SChris Fraire                     }
8287d004396SChris Fraire                     Path newAbsolute = Paths.get(entry.getAbsolute());
8297d004396SChris Fraire                     return env.getPathRelativeToSourceRoot(newAbsolute.toFile());
8307d004396SChris Fraire                 } else if (canonical.startsWith(entry.getCanonicalSeparated())) {
8317d004396SChris Fraire                     Path newAbsolute = Paths.get(entry.getAbsolute(),
8327d004396SChris Fraire                             canonical.substring(entry.getCanonicalSeparated().length()));
8337d004396SChris Fraire                     return env.getPathRelativeToSourceRoot(newAbsolute.toFile());
8347d004396SChris Fraire                 }
8357d004396SChris Fraire             }
8367d004396SChris Fraire         }
8377d004396SChris Fraire 
8387d004396SChris Fraire         return relativePath;
8397d004396SChris Fraire     }
8407d004396SChris Fraire 
ensureSettingsHelper()841497eb8c9SChris Fraire     private void ensureSettingsHelper() {
842497eb8c9SChris Fraire         if (settingsHelper == null) {
843497eb8c9SChris Fraire             settingsHelper = new SettingsHelper(reader);
844b5840353SAdam Hornáček         }
845b5840353SAdam Hornáček     }
846b5840353SAdam Hornáček }
847