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