• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3715

30 Nov 2023 08:55AM UTC coverage: 75.937% (+9.8%) from 66.106%
#3715

push

web-flow
Refactoring to reduce sonar code smell fixes (#4485)

---------

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>
Co-authored-by: Vladimir Kotal <vlada@kotalovi.cz>

397 of 478 new or added lines in 51 files covered. (83.05%)

50 existing lines in 22 files now uncovered.

44494 of 58593 relevant lines covered (75.94%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.28
/opengrok-indexer/src/main/java/org/opengrok/indexer/web/SearchHelper.java
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, 2022, 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.Optional;
36
import java.util.Set;
37
import java.util.SortedSet;
38
import java.util.TreeSet;
39
import java.util.logging.Level;
40
import java.util.logging.Logger;
41
import java.util.regex.Pattern;
42
import java.util.stream.Collectors;
43
import org.apache.lucene.document.Document;
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.opengrok.indexer.analysis.AbstractAnalyzer;
67
import org.opengrok.indexer.analysis.AnalyzerGuru;
68
import org.opengrok.indexer.analysis.CompatibleAnalyser;
69
import org.opengrok.indexer.analysis.Definitions;
70
import org.opengrok.indexer.configuration.Project;
71
import org.opengrok.indexer.configuration.RuntimeEnvironment;
72
import org.opengrok.indexer.configuration.SuperIndexSearcher;
73
import org.opengrok.indexer.index.IndexedSymlink;
74
import org.opengrok.indexer.logger.LoggerFactory;
75
import org.opengrok.indexer.search.QueryBuilder;
76
import org.opengrok.indexer.search.SettingsHelper;
77
import org.opengrok.indexer.search.Summarizer;
78
import org.opengrok.indexer.search.context.Context;
79
import org.opengrok.indexer.search.context.HistoryContext;
80
import org.opengrok.indexer.util.ForbiddenSymlinkException;
81

82
/**
83
 * Working set for a search basically to factor out/separate search related
84
 * complexity from UI design.
85
 *
86
 * @author Jens Elkner
87
 */
88
public class SearchHelper {
89

90
    private static final Logger LOGGER = LoggerFactory.getLogger(SearchHelper.class);
1✔
91

92
    private static final Pattern TAB_SPACE = Pattern.compile("[\t ]+");
1✔
93

94
    public static final String REQUEST_ATTR = "SearchHelper";
95

96
    /**
97
     * Default query parse error message prefix.
98
     */
99
    public static final String PARSE_ERROR_MSG = "Unable to parse your query: ";
100

101
    /**
102
     * Max number of words to suggest for spellcheck.
103
     */
104
    public static final int SPELLCHECK_SUGGEST_WORD_COUNT = 5;
105

106
    /**
107
     * data root: used to find the search index file.
108
     */
109
    private final File dataRoot;
110
    /**
111
     * context path, i.e. the applications' context path (usually /source) to use
112
     * when generating a redirect URL
113
     */
114
    private final String contextPath;
115

116
    /**
117
     * piggyback: the source root directory.
118
     */
119
    private final File sourceRoot;
120

121
    /**
122
     * piggyback: the <i>Eftar</i> file-reader to use.
123
     */
124
    private final EftarFileReader desc;
125
    /**
126
     * the result cursor start index, i.e. where to start displaying results
127
     */
128
    private final int start;
129
    /**
130
     * max. number of result items to show
131
     */
132
    private final int maxItems;
133
    /**
134
     * The QueryBuilder used to create the query.
135
     */
136
    private final QueryBuilder builder;
137
    /**
138
     * The order used for ordering query results.
139
     */
140
    private final SortOrder order;
141
    /**
142
     * Indicate whether this is search from a cross-reference. If {@code true}
143
     * {@link #executeQuery()} sets {@link #redirect} if certain conditions are
144
     * met.
145
     */
146
    private final boolean crossRefSearch;
147
    /**
148
     * As with {@link #crossRefSearch}, but here indicating either a
149
     * cross-reference search or a "full blown search".
150
     */
151
    private final boolean guiSearch;
152
    /**
153
     * if not {@code null}, the consumer should redirect the client to a
154
     * separate result page denoted by the value of this field. Automatically
155
     * set via {@link #prepareExec(SortedSet)} and {@link #executeQuery()}.
156
     */
157
    private String redirect;
158
    /**
159
     * A value indicating if redirection should be short-circuited when state or
160
     * query result would have indicated otherwise.
161
     */
162
    private final boolean noRedirect;
163
    /**
164
     * if not {@code null}, the UI should show this error message and stop
165
     * processing the search. Automatically set via
166
     * {@link #prepareExec(SortedSet)} and {@link #executeQuery()}.
167
     */
168
    private String errorMsg;
169
    /**
170
     * the reader used to open the index. Automatically set via
171
     * {@link #prepareExec(SortedSet)}.
172
     */
173
    private IndexReader reader;
174
    /**
175
     * the searcher used to open/search the index. Automatically set via {@link #prepareExec(SortedSet)}.
176
     */
177
    private IndexSearcher searcher;
178
    /**
179
     * If performing multi-project search, the indexSearcher objects will be
180
     * tracked by the indexSearcherMap so that they can be properly released
181
     * once the results are read.
182
     */
183
    private final ArrayList<SuperIndexSearcher> superIndexSearchers = new ArrayList<>();
1✔
184
    /**
185
     * List of docs which result from the executing the query.
186
     */
187
    private ScoreDoc[] hits;
188
    /**
189
     * Total number of hits.
190
     */
191
    private long totalHits;
192
    /**
193
     * the query created by {@link #builder} via
194
     * {@link #prepareExec(SortedSet)}.
195
     */
196
    private Query query;
197
    /**
198
     * the Lucene sort instruction based on {@link #order} created via
199
     * {@link #prepareExec(SortedSet)}.
200
     */
201
    protected Sort sort;
202
    /**
203
     * The spellchecker object.
204
     */
205
    private DirectSpellChecker checker;
206
    /**
207
     * projects to use to set up indexer searchers. Usually done via {@link #prepareExec(SortedSet)}.
208
     */
209
    private SortedSet<String> projects;
210
    /**
211
     * opengrok summary context. Usually created via {@link #prepareSummary()}.
212
     */
213
    private Context sourceContext = null;
1✔
214
    /**
215
     * result summarizer usually created via {@link #prepareSummary()}.
216
     */
217
    private Summarizer summarizer = null;
1✔
218
    /**
219
     * history context usually created via {@link #prepareSummary()}.
220
     */
221
    private HistoryContext historyContext;
222

223
    private SettingsHelper settingsHelper;
224

225
    public SearchHelper(int start, SortOrder sortOrder, File dataRoot, File sourceRoot, int maxItems,
226
                        EftarFileReader eftarFileReader, QueryBuilder queryBuilder, boolean crossRefSearch,
227
                        String contextPath, boolean guiSearch, boolean noRedirect) {
1✔
228
        this.start = start;
1✔
229
        this.order = sortOrder;
1✔
230
        this.dataRoot = dataRoot;
1✔
231
        this.sourceRoot = sourceRoot;
1✔
232
        this.maxItems = maxItems;
1✔
233
        this.desc = eftarFileReader;
1✔
234
        this.builder = queryBuilder;
1✔
235
        this.crossRefSearch = crossRefSearch;
1✔
236
        this.contextPath = contextPath;
1✔
237
        this.guiSearch = guiSearch;
1✔
238
        this.noRedirect = noRedirect;
1✔
239
    }
1✔
240

241
    public File getDataRoot() {
242
        return dataRoot;
×
243
    }
244

245
    public File getSourceRoot() {
246
        return sourceRoot;
×
247
    }
248

249
    public EftarFileReader getDesc() {
250
        return desc;
×
251
    }
252

253
    public QueryBuilder getBuilder() {
254
        return builder;
1✔
255
    }
256

257
    public String getContextPath() {
258
        return contextPath;
×
259
    }
260

261
    public void setRedirect(String redirect) {
262
        this.redirect = redirect;
×
263
    }
×
264

265
    public String getRedirect() {
266
        return redirect;
×
267
    }
268

269
    public String getErrorMsg() {
270
        return errorMsg;
1✔
271
    }
272

273
    public void setErrorMsg(String errorMsg) {
274
        this.errorMsg = errorMsg;
×
275
    }
×
276

277
    public IndexSearcher getSearcher() {
278
        return searcher;
×
279
    }
280

281
    public ScoreDoc[] getHits() {
282
        return hits;
×
283
    }
284

285
    public Query getQuery() {
286
        return query;
×
287
    }
288

289
    public long getTotalHits() {
290
        return totalHits;
1✔
291
    }
292

293
    public SortedSet<String> getProjects() {
294
        return projects;
×
295
    }
296

297
    public Context getSourceContext() {
298
        return sourceContext;
×
299
    }
300

301
    public int getMaxItems() {
302
        return maxItems;
×
303
    }
304

305
    public SortOrder getOrder() {
306
        return order;
×
307
    }
308

309
    public int getStart() {
310
        return start;
×
311
    }
312

313
    public Summarizer getSummarizer() {
314
        return summarizer;
×
315
    }
316

317
    public HistoryContext getHistoryContext() {
318
        return historyContext;
×
319
    }
320

321
    /**
322
     * User readable description for file types. Only those listed in
323
     * fileTypeDescription will be shown to the user.
324
     *
325
     * Returns a set of file type descriptions to be used for a search form.
326
     *
327
     * @return Set of tuples with file type and description.
328
     */
329
    public static Set<Map.Entry<String, String>> getFileTypeDescriptions() {
330
        return AnalyzerGuru.getfileTypeDescriptions().entrySet();
×
331
    }
332

333
    /**
334
     * Create the searcher to use w.r.t. currently set parameters and the given
335
     * projects. Does not produce any {@link #redirect} link. It also does
336
     * nothing if {@link #redirect} or {@link #errorMsg} have a none-{@code null} value.
337
     * <p>
338
     * Parameters which should be populated/set at this time:
339
     * <ul>
340
     * <li>{@link #builder}</li> <li>{@link #dataRoot}</li>
341
     * <li>{@link #order} (falls back to relevance if unset)</li>
342
     * </ul>
343
     * Populates/sets:
344
     * <ul>
345
     * <li>{@link #query}</li> <li>{@link #searcher}</li> <li>{@link #sort}</li>
346
     * <li>{@link #projects}</li> <li>{@link #errorMsg} if an error occurs</li>
347
     * </ul>
348
     *
349
     * @param projects project names. If empty, a no-project setup is assumed (i.e. DATA_ROOT/index will be used
350
     *                 instead of possible multiple DATA_ROOT/$project/index). If the set contains projects
351
     *                 not known in the configuration or projects not yet indexed an error will be returned
352
     *                 in {@link #errorMsg}.
353
     * @return this instance
354
     */
355
    public SearchHelper prepareExec(SortedSet<String> projects) {
356
        if (redirect != null || errorMsg != null) {
1✔
357
            return this;
×
358
        }
359

360
        settingsHelper = null;
1✔
361
        // the Query created by the QueryBuilder
362
        try {
363
            query = builder.build();
1✔
364
            if (projects == null) {
1✔
365
                errorMsg = "No project selected!";
×
366
                return this;
×
367
            }
368
            this.projects = projects;
1✔
369
            if (projects.isEmpty()) {
1✔
370
                // no project setup
371
                SuperIndexSearcher superIndexSearcher = RuntimeEnvironment.getInstance().getSuperIndexSearcher("");
×
372
                searcher = superIndexSearcher;
×
373
                superIndexSearchers.add(superIndexSearcher);
×
374
                reader = superIndexSearcher.getIndexReader();
×
375
            } else {
×
376
                // Check list of project names first to make sure all of them are valid and indexed.
377
                Set<String> invalidProjects = projects.stream().
1✔
378
                    filter(proj -> (Project.getByName(proj) == null)).
1✔
379
                    collect(Collectors.toSet());
1✔
380
                if (!invalidProjects.isEmpty()) {
1✔
381
                    errorMsg = "Project list contains invalid projects: " +
1✔
382
                        String.join(", ", invalidProjects);
1✔
383
                    return this;
1✔
384
                }
385
                Set<Project> notIndexedProjects =
1✔
386
                    projects.stream().
1✔
387
                    map(Project::getByName).
1✔
388
                    filter(proj -> !proj.isIndexed()).
1✔
389
                    collect(Collectors.toSet());
1✔
390
                if (!notIndexedProjects.isEmpty()) {
1✔
391
                    errorMsg = "Some of the projects to be searched are not indexed yet: " +
1✔
392
                        String.join(", ", notIndexedProjects.stream().
1✔
393
                        map(Project::getName).
1✔
394
                        collect(Collectors.toSet()));
1✔
395
                    return this;
1✔
396
                }
397

398
                // We use MultiReader even for single project. This should not matter
399
                // given that MultiReader is just a cheap wrapper around set of IndexReader objects.
400
                reader = RuntimeEnvironment.getInstance().getMultiReader(projects, superIndexSearchers);
1✔
401
                if (reader != null) {
1✔
402
                    searcher = RuntimeEnvironment.getInstance().getIndexSearcherFactory().newSearcher(reader);
1✔
403
                } else {
404
                    errorMsg = "Failed to initialize search. Check the index";
×
405
                    if (!projects.isEmpty()) {
×
406
                        errorMsg += " for projects: " + String.join(", ", projects);
×
407
                    }
408
                    return this;
×
409
                }
410
            }
411

412
            // TODO check if below is somehow reusing sessions so we don't
413
            // requery again and again, I guess 2min timeout sessions could be
414
            // useful, since you click on the next page within 2mins, if not,
415
            // then wait ;)
416
            // Most probably they are not reused. SearcherLifetimeManager might help here.
417
            switch (order) {
1✔
418
                case LASTMODIFIED:
419
                    sort = new Sort(new SortField(QueryBuilder.DATE, SortField.Type.STRING, true));
×
420
                    break;
×
421
                case BY_PATH:
422
                    sort = new Sort(new SortField(QueryBuilder.FULLPATH, SortField.Type.STRING));
×
423
                    break;
×
424
                default:
425
                    sort = Sort.RELEVANCE;
1✔
426
                    break;
427
            }
428
            checker = new DirectSpellChecker();
1✔
429
        } catch (ParseException e) {
×
430
            errorMsg = PARSE_ERROR_MSG + e.getMessage();
×
431
        } catch (FileNotFoundException e) {
×
432
            errorMsg = "Index database not found. Check the index";
×
433
            if (!projects.isEmpty()) {
×
434
                errorMsg += " for projects: " + String.join(", ", projects);
×
435
            }
436
            errorMsg += "; " + e.getMessage();
×
437
        } catch (IOException e) {
×
438
            errorMsg = e.getMessage();
×
439
        }
1✔
440
        return this;
1✔
441
    }
442

443
    /**
444
     * Calls {@link #prepareExec(java.util.SortedSet)} with a single-element
445
     * set for {@code project}.
446
     * @param project a defined instance
447
     * @return this instance
448
     */
449
    public SearchHelper prepareExec(Project project) {
450
        SortedSet<String> oneProject = new TreeSet<>();
×
451
        oneProject.add(project.getName());
×
452
        return prepareExec(oneProject);
×
453
    }
454

455
    /**
456
     * Start the search prepared by {@link #prepareExec(SortedSet)}. It does
457
     * nothing if {@link #redirect} or {@link #errorMsg} have a
458
     * none-{@code null} value.
459
     * <p>
460
     * Parameters which should be populated/set at this time: <ul> <li>all
461
     * fields required for and populated by
462
     * {@link #prepareExec(SortedSet)})</li> <li>{@link #start} (default:
463
     * 0)</li> <li>{@link #maxItems} (default: 0)</li>
464
     * <li>{@link #crossRefSearch} (default: false)</li> </ul> Populates/sets:
465
     * <ul> <li>{@link #hits} (see {@link TopFieldDocs#scoreDocs})</li>
466
     * <li>{@link #totalHits} (see {@link TopFieldDocs#totalHits})</li>
467
     * <li>{@link #contextPath}</li> <li>{@link #errorMsg} if an error
468
     * occurs</li> <li>{@link #redirect} if certain conditions are met</li>
469
     * </ul>
470
     *
471
     * @return this instance
472
     */
473
    public SearchHelper executeQuery() {
474
        if (redirect != null || errorMsg != null) {
1✔
475
            return this;
×
476
        }
477
        try {
478
            TopFieldDocs fdocs = searcher.search(query, start + maxItems, sort);
1✔
479
            totalHits = fdocs.totalHits.value;
1✔
480
            hits = fdocs.scoreDocs;
1✔
481

482
            /*
483
             * Determine if possibly a single-result redirect to xref is
484
             * eligible and applicable. If history query is active, then nope.
485
             */
486
            if (!noRedirect && hits != null && hits.length == 1 && builder.getHist() == null) {
1✔
487
                int docID = hits[0].doc;
×
488
                if (crossRefSearch && query instanceof TermQuery && builder.getDefs() != null) {
×
489
                    maybeRedirectToDefinition(docID, (TermQuery) query);
×
490
                } else if (guiSearch) {
×
491
                    if (builder.isPathSearch()) {
×
492
                        redirectToFile(docID);
×
493
                    } else {
494
                        maybeRedirectToMatchOffset(docID, builder.getContextFields());
×
495
                    }
496
                }
497
            }
498
        } catch (IOException | ClassNotFoundException e) {
×
499
            errorMsg = e.getMessage();
×
500
        }
1✔
501
        return this;
1✔
502
    }
503

504
    private void maybeRedirectToDefinition(int docID, TermQuery termQuery)
505
            throws IOException, ClassNotFoundException {
506
        // Bug #3900: Check if this is a search for a single term, and that
507
        // term is a definition. If that's the case, and we only have one match,
508
        // we'll generate a direct link instead of a listing.
509
        //
510
        // Attempt to create a direct link to the definition if we search for
511
        // one single definition term AND we have exactly one match AND there
512
        // is only one definition of that symbol in the document that matches.
513
        Document doc = searcher.storedFields().document(docID);
×
514
        IndexableField tagsField = doc.getField(QueryBuilder.TAGS);
×
515
        if (tagsField != null) {
×
516
            byte[] rawTags = tagsField.binaryValue().bytes;
×
517
            Definitions tags = Definitions.deserialize(rawTags);
×
518
            String symbol = termQuery.getTerm().text();
×
519
            if (tags.occurrences(symbol) == 1) {
×
520
                String anchor = Util.uriEncode(symbol);
×
521
                redirect = contextPath + Prefix.XREF_P
×
522
                        + Util.uriEncodePath(doc.get(QueryBuilder.PATH))
×
523
                        + '?' + QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ + anchor
524
                        + '#' + anchor;
525
            }
526
        }
527
    }
×
528

529
    private void maybeRedirectToMatchOffset(int docID, List<String> contextFields)
530
            throws IOException {
531
        /*
532
         * Only PLAIN files might redirect to a file offset, since an offset
533
         * must be subsequently converted to a line number and that is tractable
534
         * only from plain text.
535
         */
536
        Document doc = searcher.storedFields().document(docID);
×
537
        String genre = doc.get(QueryBuilder.T);
×
538
        if (!AbstractAnalyzer.Genre.PLAIN.typeName().equals(genre)) {
×
539
            return;
×
540
        }
541

542
        List<LeafReaderContext> leaves = reader.leaves();
×
543
        int subIndex = ReaderUtil.subIndex(docID, leaves);
×
544
        LeafReaderContext leaf = leaves.get(subIndex);
×
545

546
        Query rewritten = query.rewrite(searcher);
×
547
        Weight weight = rewritten.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1);
×
548
        Matches matches = weight.matches(leaf, docID - leaf.docBase); // Adjust docID
×
549
        if (matches != null && matches != MatchesUtils.MATCH_WITH_NO_TERMS) {
×
550
            int matchCount = 0;
×
551
            int offset = -1;
×
552
            for (String field : contextFields) {
×
553
                MatchesIterator matchesIterator = matches.getMatches(field);
×
554
                while (matchesIterator.next()) {
×
555
                    if (matchesIterator.startOffset() >= 0) {
×
556
                        // Abort if there is more than a single match offset.
557
                        if (++matchCount > 1) {
×
558
                            return;
×
559
                        }
560
                        offset = matchesIterator.startOffset();
×
561
                    }
562
                }
563
            }
×
564
            if (offset >= 0) {
×
565
                redirect = contextPath + Prefix.XREF_P
×
566
                        + Util.uriEncodePath(doc.get(QueryBuilder.PATH))
×
567
                        + '?' + QueryParameters.MATCH_OFFSET_PARAM_EQ + offset;
568
            }
569
        }
570
    }
×
571

572
    private void redirectToFile(int docID) throws IOException {
573
        Document doc = searcher.storedFields().document(docID);
×
574
        redirect = contextPath + Prefix.XREF_P + Util.uriEncodePath(doc.get(QueryBuilder.PATH));
×
575
    }
×
576

577
    private void getSuggestion(Term term, IndexReader ir,
578
            List<String> result) throws IOException {
579
        if (term == null) {
×
580
            return;
×
581
        }
582
        String[] toks = TAB_SPACE.split(term.text(), 0);
×
583
        for (String tok : toks) {
×
584
            //TODO below seems to be case insensitive ... for refs/defs this is bad
585
            SuggestWord[] words = checker.suggestSimilar(new Term(term.field(), tok),
×
586
                SPELLCHECK_SUGGEST_WORD_COUNT, ir, SuggestMode.SUGGEST_ALWAYS);
587
            for (SuggestWord w : words) {
×
588
                result.add(w.string);
×
589
            }
590
        }
591
    }
×
592

593
    /**
594
     * If a search did not return a hit, one may use this method to obtain
595
     * suggestions for a new search.
596
     *
597
     * <p>
598
     * Parameters which should be populated/set at this time: <ul>
599
     * <li>{@link #projects}</li> <li>{@link #dataRoot}</li>
600
     * <li>{@link #builder}</li> </ul>
601
     *
602
     * @return a possible empty list of suggestions.
603
     */
604
    public List<Suggestion> getSuggestions() {
605
        if (projects == null) {
×
606
            return new ArrayList<>(0);
×
607
        }
608

609
        boolean emptyProjects = false;
×
610
        String[] projectNames;
611
        if (projects.isEmpty()) {
×
612
            projectNames = new String[]{"/"};
×
613
            emptyProjects = true;
×
614
        } else if (projects.size() == 1) {
×
615
            projectNames = new String[]{projects.first()};
×
616
        } else {
617
            projectNames = new String[projects.size()];
×
618
            int ii = 0;
×
619
            for (String proj : projects) {
×
620
                projectNames[ii++] = proj;
×
621
            }
×
622
        }
623

624
        List<Suggestion> res = new ArrayList<>();
×
625
        List<String> dummy = new ArrayList<>();
×
626
        IndexReader ir = null;
×
627
        Term t;
628
        for (String projectName : projectNames) {
×
629
            Suggestion suggestion = new Suggestion(projectName);
×
630
            try {
631
                SuperIndexSearcher superIndexSearcher;
632
                if (emptyProjects) {
×
633
                    superIndexSearcher = RuntimeEnvironment.getInstance().getSuperIndexSearcher("");
×
634
                } else {
635
                    superIndexSearcher = RuntimeEnvironment.getInstance().getSuperIndexSearcher(projectName);
×
636
                }
637
                superIndexSearchers.add(superIndexSearcher);
×
638
                ir = superIndexSearcher.getIndexReader();
×
639

640
                if (builder.getFreetext() != null && !builder.getFreetext().isEmpty()) {
×
641
                    t = new Term(QueryBuilder.FULL, builder.getFreetext());
×
642
                    getSuggestion(t, ir, dummy);
×
643
                    suggestion.setFreetext(dummy.toArray(new String[0]));
×
644
                    dummy.clear();
×
645
                }
646
                if (builder.getRefs() != null && !builder.getRefs().isEmpty()) {
×
647
                    t = new Term(QueryBuilder.REFS, builder.getRefs());
×
648
                    getSuggestion(t, ir, dummy);
×
649
                    suggestion.setRefs(dummy.toArray(new String[0]));
×
650
                    dummy.clear();
×
651
                }
652
                if (builder.getDefs() != null && !builder.getDefs().isEmpty()) {
×
653
                    t = new Term(QueryBuilder.DEFS, builder.getDefs());
×
654
                    getSuggestion(t, ir, dummy);
×
655
                    suggestion.setDefs(dummy.toArray(new String[0]));
×
656
                    dummy.clear();
×
657
                }
658
                //TODO suggest also for path and history?
659
                if (suggestion.isUsable()) {
×
660
                    res.add(suggestion);
×
661
                }
662
            } catch (IOException e) {
×
663
                LOGGER.log(Level.WARNING,
×
664
                        String.format("Got exception while getting spelling suggestions for project %s:", projectName),
×
665
                        e);
666
            }
×
667
        }
668

669
        return res;
×
670
    }
671

672
    /**
673
     * Prepare the fields to support printing a full blown summary. Does nothing
674
     * if {@link #redirect} or {@link #errorMsg} have a none-{@code null} value.
675
     *
676
     * <p>
677
     * Parameters which should be populated/set at this time: <ul>
678
     * <li>{@link #query}</li> <li>{@link #builder}</li> </ul> Populates/sets:
679
     * Otherwise the following fields are set (includes {@code null}): <ul>
680
     * <li>{@link #sourceContext}</li> <li>{@link #summarizer}</li>
681
     * <li>{@link #historyContext}</li> </ul>
682
     *
683
     * @return this instance.
684
     */
685
    public SearchHelper prepareSummary() {
686
        if (redirect != null || errorMsg != null) {
1✔
687
            return this;
×
688
        }
689
        try {
690
            sourceContext = new Context(query, builder);
1✔
691
            summarizer = new Summarizer(query, new CompatibleAnalyser());
1✔
692
        } catch (Exception e) {
×
693
            LOGGER.log(Level.WARNING, "Summarizer: {0}", e.getMessage());
×
694
        }
1✔
695
        try {
696
            historyContext = new HistoryContext(query);
1✔
697
        } catch (Exception e) {
×
698
            LOGGER.log(Level.WARNING, "HistoryContext: {0}", e.getMessage());
×
699
        }
1✔
700
        return this;
1✔
701
    }
702

703
    /**
704
     * Free any resources associated with this helper.
705
     */
706
    public void destroy() {
707
        for (SuperIndexSearcher superIndexSearcher : superIndexSearchers) {
1✔
708
            try {
709
                superIndexSearcher.release();
1✔
710
            } catch (IOException ex) {
×
711
                LOGGER.log(Level.WARNING, "cannot release SuperIndexSearcher", ex);
×
712
            }
1✔
713
        }
1✔
714
    }
1✔
715

716
    /**
717
     * Searches for a document for a single file from the index.
718
     * @param file the file whose definitions to find
719
     * @return {@link ScoreDoc#doc} or -1 if it could not be found
720
     * @throws IOException if an error happens when accessing the index
721
     * @throws ParseException if an error happens when building the Lucene query
722
     */
723
    public int searchSingle(File file) throws IOException,
724
            ParseException {
725

726
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
×
727
        String path;
728
        try {
729
            path = env.getPathRelativeToSourceRoot(file);
×
730
        } catch (ForbiddenSymlinkException e) {
×
731
            LOGGER.log(Level.FINER, e.getMessage());
×
732
            return -1;
×
733
        }
×
734
        //sanitize windows path delimiters
735
        //in order not to conflict with Lucene escape character
736
        path = path.replace("\\", "/");
×
737

738
        QueryBuilder singleBuilder = new QueryBuilder();
×
739
        if (builder != null) {
×
740
            singleBuilder.reset(builder);
×
741
        }
742
        query = singleBuilder.setPath(path).build();
×
743

744
        TopDocs top = searcher.search(query, 1);
×
745
        if (top.totalHits.value == 0) {
×
746
            return -1;
×
747
        }
748

749
        int docID = top.scoreDocs[0].doc;
×
750
        Document doc = searcher.storedFields().document(docID);
×
751

752
        String foundPath = doc.get(QueryBuilder.PATH);
×
753
        // Only use the result if PATH matches exactly.
754
        if (!path.equals(foundPath)) {
×
755
            return -1;
×
756
        }
757

758
        return docID;
×
759
    }
760

761
    /**
762
     * Gets the persisted tabSize via {@link SettingsHelper} for the active
763
     * reader.
764
     * @param proj a defined instance or {@code null} if no project is active
765
     * @return tabSize
766
     * @throws IOException if an I/O error occurs querying the active reader
767
     */
768
    public int getTabSize(Project proj) throws IOException {
769
        ensureSettingsHelper();
×
770
        return settingsHelper.getTabSize(proj);
×
771
    }
772

773
    /**
774
     * Determines if there is a prime equivalent to {@code relativePath}
775
     * according to indexed symlinks and translate (or not) accordingly.
776
     * @param project the project name or empty string if projects are not used
777
     * @param relativePath an OpenGrok-style (i.e. starting with a file
778
     *                     separator) relative path
779
     * @return a prime relative path or just {@code relativePath} if no prime
780
     * is matched
781
     */
782
    public String getPrimeRelativePath(String project, String relativePath)
783
            throws IOException, ForbiddenSymlinkException {
784

785
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
×
NEW
786
        String nonEmptySourceRoot = Optional.ofNullable(env.getSourceRootPath())
×
NEW
787
                .orElseThrow(() -> new IllegalStateException("sourceRoot is not defined"));
×
788

NEW
789
        File absolute = new File(nonEmptySourceRoot + relativePath);
×
790

791
        ensureSettingsHelper();
×
792
        settingsHelper.getSettings(project);
×
793
        Map<String, IndexedSymlink> indexedSymlinks = settingsHelper.getSymlinks(project);
×
NEW
794
        String canonical = absolute.getCanonicalFile().getPath();
×
NEW
795
        for (IndexedSymlink entry : indexedSymlinks.values()) {
×
NEW
796
            if (canonical.equals(entry.getCanonical())) {
×
NEW
797
                if (absolute.getPath().equals(entry.getAbsolute())) {
×
NEW
798
                    return relativePath;
×
799
                }
NEW
800
                Path newAbsolute = Paths.get(entry.getAbsolute());
×
NEW
801
                return env.getPathRelativeToSourceRoot(newAbsolute.toFile());
×
NEW
802
            } else if (canonical.startsWith(entry.getCanonicalSeparated())) {
×
NEW
803
                Path newAbsolute = Paths.get(entry.getAbsolute(),
×
NEW
804
                        canonical.substring(entry.getCanonicalSeparated().length()));
×
NEW
805
                return env.getPathRelativeToSourceRoot(newAbsolute.toFile());
×
806
            }
UNCOV
807
        }
×
808

809
        return relativePath;
×
810
    }
811

812
    private void ensureSettingsHelper() {
813
        if (settingsHelper == null) {
×
814
            settingsHelper = new SettingsHelper(reader);
×
815
        }
816
    }
×
817
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc