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