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

oracle / opengrok / #3669

01 Nov 2023 10:10AM UTC coverage: 75.13% (-0.03%) from 75.16%
#3669

push

web-flow
Fix Sonar codesmell issues (#4460)

Signed-off-by: Gino Augustine <ginoaugustine@gmail.com>

308 of 308 new or added lines in 27 files covered. (100.0%)

44029 of 58604 relevant lines covered (75.13%)

0.75 hits per line

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

72.27
/suggester/src/main/java/org/opengrok/suggest/Suggester.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) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
22
 */
23
package org.opengrok.suggest;
24

25
import io.micrometer.core.instrument.MeterRegistry;
26
import io.micrometer.core.instrument.Timer;
27
import org.apache.commons.lang3.time.DurationFormatUtils;
28
import org.apache.lucene.index.DirectoryReader;
29
import org.apache.lucene.index.IndexReader;
30
import org.apache.lucene.index.Term;
31
import org.apache.lucene.search.Query;
32
import org.apache.lucene.store.Directory;
33
import org.apache.lucene.store.FSDirectory;
34
import org.apache.lucene.util.BytesRef;
35
import org.opengrok.suggest.query.SuggesterPrefixQuery;
36
import org.opengrok.suggest.query.SuggesterQuery;
37
import org.opengrok.suggest.util.Progress;
38

39
import java.io.Closeable;
40
import java.io.File;
41
import java.io.IOException;
42
import java.nio.file.Path;
43
import java.time.Duration;
44
import java.time.Instant;
45
import java.util.ArrayList;
46
import java.util.Collection;
47
import java.util.Collections;
48
import java.util.HashSet;
49
import java.util.List;
50
import java.util.Map;
51
import java.util.Map.Entry;
52
import java.util.Objects;
53
import java.util.Optional;
54
import java.util.Set;
55
import java.util.concurrent.Callable;
56
import java.util.concurrent.ConcurrentHashMap;
57
import java.util.concurrent.CountDownLatch;
58
import java.util.concurrent.ExecutorService;
59
import java.util.concurrent.Executors;
60
import java.util.concurrent.Future;
61
import java.util.concurrent.TimeUnit;
62
import java.util.concurrent.locks.Condition;
63
import java.util.concurrent.locks.Lock;
64
import java.util.concurrent.locks.ReentrantLock;
65
import java.util.logging.Level;
66
import java.util.logging.Logger;
67
import java.util.stream.Collectors;
68
import java.util.stream.Stream;
69

70
/**
71
 * Provides an interface for accessing suggester functionality.
72
 */
73
public final class Suggester implements Closeable {
74

75
    private static final String PROJECTS_DISABLED_KEY = "";
76

77
    private static final Logger LOGGER = Logger.getLogger(Suggester.class.getName());
1✔
78

79
    private final Map<String, SuggesterProjectData> projectData = new ConcurrentHashMap<>();
1✔
80

81
    private final Object lock = new Object();
1✔
82

83
    private final File suggesterDir;
84

85
    private int resultSize;
86

87
    private Duration awaitTerminationTime;
88

89
    private final boolean allowMostPopular;
90

91
    private final boolean projectsEnabled;
92

93
    private final Set<String> allowedFields;
94

95
    private final int timeThreshold;
96

97
    private final int rebuildParallelismLevel;
98
    private final boolean isPrintProgress;
99

100
    private volatile boolean rebuilding;
101
    private volatile boolean terminating;
102
    private final Lock rebuildLock = new ReentrantLock();
1✔
103
    private final Condition rebuildDone = rebuildLock.newCondition();
1✔
104

105
    private final CountDownLatch initDone = new CountDownLatch(1);
1✔
106

107
    private final Timer suggesterRebuildTimer;
108
    private final Timer suggesterInitTimer;
109

110
    // do NOT use fork join thread pool (work stealing thread pool) because it does not send interrupts upon cancellation
111
    private final ExecutorService executorService = Executors.newFixedThreadPool(
1✔
112
            Runtime.getRuntime().availableProcessors(),
1✔
113
            runnable -> {
114
                Thread thread = Executors.defaultThreadFactory().newThread(runnable);
1✔
115
                // This should match the naming in OpenGrokThreadFactory class.
116
                thread.setName("OpenGrok-suggester-lookup-" + thread.getId());
1✔
117
                return thread;
1✔
118
            });
119

120
    /**
121
     * @param suggesterDir directory under which the suggester data should be created
122
     * @param resultSize maximum number of items that should be returned
123
     * @param awaitTerminationTime how much time to wait for suggester to initialize
124
     * @param allowMostPopular specifies if the most popular completion is enabled
125
     * @param projectsEnabled specifies if the OpenGrok projects are enabled
126
     * @param allowedFields fields for which should the suggester be enabled,
127
     * if {@code null} then enabled for all fields
128
     * @param timeThreshold time in milliseconds after which the suggestions requests should time out
129
     * @param rebuildParallelismLevel parallelism level for rebuild
130
     * @param registry meter registry
131
     * @param isPrintProgress whether to report progress for initialization and rebuild
132
     */
133
    @SuppressWarnings("java:S107")
134
    public Suggester(
135
            final File suggesterDir,
136
            final int resultSize,
137
            final Duration awaitTerminationTime,
138
            final boolean allowMostPopular,
139
            final boolean projectsEnabled,
140
            final Set<String> allowedFields,
141
            final int timeThreshold,
142
            final int rebuildParallelismLevel,
143
            MeterRegistry registry,
144
            boolean isPrintProgress) {
1✔
145
        if (suggesterDir == null) {
1✔
146
            throw new IllegalArgumentException("Suggester needs to have directory specified");
1✔
147
        }
148
        if (suggesterDir.exists() && !suggesterDir.isDirectory()) {
1✔
149
            throw new IllegalArgumentException(suggesterDir + " is not a directory");
1✔
150
        }
151

152
        this.suggesterDir = suggesterDir;
1✔
153

154
        setResultSize(resultSize);
1✔
155
        setAwaitTerminationTime(awaitTerminationTime);
1✔
156

157
        this.allowMostPopular = allowMostPopular;
1✔
158
        this.projectsEnabled = projectsEnabled;
1✔
159
        this.allowedFields = new HashSet<>(allowedFields);
1✔
160
        this.timeThreshold = timeThreshold;
1✔
161
        this.rebuildParallelismLevel = rebuildParallelismLevel;
1✔
162
        this.isPrintProgress = isPrintProgress;
1✔
163

164
        suggesterRebuildTimer = Timer.builder("suggester.rebuild.latency").
1✔
165
                description("suggester rebuild latency").
1✔
166
                register(registry);
1✔
167
        suggesterInitTimer = Timer.builder("suggester.init.latency").
1✔
168
                description("suggester initialization latency").
1✔
169
                register(registry);
1✔
170
    }
1✔
171

172
    /**
173
     * Initializes suggester data for specified indexes. The data is initialized asynchronously.
174
     * @param luceneIndexes paths to Lucene indexes and name with which the index should be associated
175
     */
176
    public void init(final Collection<NamedIndexDir> luceneIndexes) {
177
        if (luceneIndexes == null || luceneIndexes.isEmpty()) {
1✔
178
            LOGGER.log(Level.INFO, "No index directories found, exiting...");
×
179
            return;
×
180
        }
181
        if (!projectsEnabled && luceneIndexes.size() > 1) {
1✔
182
            throw new IllegalArgumentException("Projects are not enabled and multiple Lucene indexes were passed");
×
183
        }
184

185
        synchronized (lock) {
1✔
186
            Instant start = Instant.now();
1✔
187
            LOGGER.log(Level.INFO, "Initializing suggester");
1✔
188

189
            ExecutorService executor = Executors.newWorkStealingPool(rebuildParallelismLevel);
1✔
190

191
            try (Progress progress = new Progress(LOGGER, "suggester initialization", luceneIndexes.size(),
1✔
192
                    Level.INFO, isPrintProgress)) {
193
                for (NamedIndexDir indexDir : luceneIndexes) {
1✔
194
                    if (terminating) {
1✔
195
                        LOGGER.log(Level.INFO, "Terminating suggester initialization");
×
196
                        return;
×
197
                    }
198
                    submitInitIfIndexExists(executor, indexDir, progress);
1✔
199
                }
1✔
200

201
                shutdownAndAwaitTermination(executor, start, suggesterInitTimer,
1✔
202
                        "Suggester successfully initialized");
203
                initDone.countDown();
1✔
204
            }
×
205
        }
1✔
206
    }
1✔
207

208
    /**
209
     * wait for initialization to finish.
210
     * @param timeout timeout value
211
     * @param unit timeout unit
212
     * @throws InterruptedException on canceled await()
213
     */
214
    public void waitForInit(long timeout, TimeUnit unit) throws InterruptedException {
215
        if (!initDone.await(timeout, unit)) {
×
216
            LOGGER.log(Level.WARNING, "Initialization did not finish in {0} {1}", new Object[] {timeout, unit});
×
217
        }
218
    }
×
219

220
    private void submitInitIfIndexExists(final ExecutorService executorService, final NamedIndexDir indexDir,
221
                                         Progress progress) {
222
        try {
223
            if (indexExists(indexDir.path)) {
1✔
224
                executorService.submit(getInitRunnable(indexDir, progress));
1✔
225
            } else {
226
                LOGGER.log(Level.FINE, "Index in ''{0}'' directory does not exist, skipping...", indexDir);
×
227
            }
228
        } catch (IOException e) {
×
229
            LOGGER.log(Level.WARNING, String.format("Could not check if index in '%s' exists", indexDir), e);
×
230
        }
1✔
231
    }
1✔
232

233
    private Runnable getInitRunnable(final NamedIndexDir indexDir, Progress progress) {
234
        return () -> {
1✔
235
            try {
236
                if (terminating) {
1✔
237
                    return;
×
238
                }
239

240
                Instant start = Instant.now();
1✔
241
                LOGGER.log(Level.FINE, "Initializing suggester data in ''{0}''", indexDir);
1✔
242

243
                SuggesterProjectData wfst = new SuggesterProjectData(FSDirectory.open(indexDir.path),
1✔
244
                        getSuggesterDir(indexDir.name), allowMostPopular, allowedFields);
1✔
245
                wfst.init();
1✔
246
                if (projectsEnabled) {
1✔
247
                    projectData.put(indexDir.name, wfst);
1✔
248
                } else {
249
                    projectData.put(PROJECTS_DISABLED_KEY, wfst);
×
250
                }
251

252
                Duration d = Duration.between(start, Instant.now());
1✔
253
                LOGGER.log(Level.FINE, "Finished initialization of suggester data in ''{0}'', took {1}",
1✔
254
                        new Object[] {indexDir, d});
255
                progress.increment();
1✔
256
            } catch (Exception e) {
×
257
                LOGGER.log(Level.SEVERE, String.format("Could not initialize suggester data for '%s'", indexDir), e);
×
258
            }
1✔
259
        };
1✔
260
    }
261

262
    private Path getSuggesterDir(final String indexDirName) {
263
        if (projectsEnabled) {
1✔
264
            return suggesterDir.toPath().resolve(indexDirName);
1✔
265
        } else {
266
            return this.suggesterDir.toPath();
×
267
        }
268
    }
269

270
    private boolean indexExists(final Path indexDir) throws IOException {
271
        try (Directory indexDirectory = FSDirectory.open(indexDir)) {
1✔
272
            return DirectoryReader.indexExists(indexDirectory);
1✔
273
        }
274
    }
275

276
    private void shutdownAndAwaitTermination(final ExecutorService executorService, Instant start,
277
                                             Timer timer,
278
                                             final String logMessageOnSuccess) {
279
        executorService.shutdown();
1✔
280
        try {
281
            executorService.awaitTermination(awaitTerminationTime.toMillis(), TimeUnit.MILLISECONDS);
1✔
282
            Duration duration = Duration.between(start, Instant.now());
1✔
283
            timer.record(duration);
1✔
284
            LOGGER.log(Level.INFO, "{0} (took {1})", new Object[]{logMessageOnSuccess,
1✔
285
                    DurationFormatUtils.formatDurationWords(duration.toMillis(),
1✔
286
                            true, true)});
287
        } catch (InterruptedException e) {
×
288
            LOGGER.log(Level.SEVERE, "Interrupted while building suggesters", e);
×
289
            Thread.currentThread().interrupt();
×
290
        }
1✔
291
    }
1✔
292

293
    /**
294
     * Rebuilds the data structures for specified indexes.
295
     * @param indexDirs paths to lucene indexes and name with which the index should be associated
296
     */
297
    public void rebuild(final Collection<NamedIndexDir> indexDirs) {
298
        if (indexDirs == null || indexDirs.isEmpty()) {
1✔
299
            LOGGER.log(Level.INFO, "Not rebuilding suggester data because no index directories were specified");
×
300
            return;
×
301
        }
302

303
        rebuildLock.lock();
1✔
304
        rebuilding = true;
1✔
305
        rebuildLock.unlock();
1✔
306

307
        synchronized (lock) {
1✔
308
            if (terminating) {
1✔
309
                return;
×
310
            }
311

312
            Instant start = Instant.now();
1✔
313
            LOGGER.log(Level.INFO, "Rebuilding the following suggesters: {0}", indexDirs);
1✔
314

315
            ExecutorService executor = Executors.newWorkStealingPool(rebuildParallelismLevel);
1✔
316

317
            try (Progress progress = new Progress(LOGGER, "suggester rebuild", indexDirs.size(),
1✔
318
                    Level.INFO, isPrintProgress)) {
319
                for (NamedIndexDir indexDir : indexDirs) {
1✔
320
                    SuggesterProjectData data = this.projectData.get(indexDir.name);
1✔
321
                    if (data != null) {
1✔
322
                        executor.submit(getRebuildRunnable(data, progress));
1✔
323
                    } else {
324
                        submitInitIfIndexExists(executor, indexDir, progress);
×
325
                    }
326
                }
1✔
327

328
                shutdownAndAwaitTermination(executor, start, suggesterRebuildTimer,
1✔
329
                        "Suggesters for " + indexDirs + " were successfully rebuilt");
330
            }
331
        }
1✔
332

333
        rebuildLock.lock();
1✔
334
        try {
335
            rebuilding = false;
1✔
336
            rebuildDone.signalAll();
1✔
337
        } finally {
338
            rebuildLock.unlock();
1✔
339
        }
340
    }
1✔
341

342
    /**
343
     * wait for rebuild to finish.
344
     * @param timeout timeout value
345
     * @param unit timeout unit
346
     * @throws InterruptedException on canceled await()
347
     */
348
    public void waitForRebuild(long timeout, TimeUnit unit) throws InterruptedException {
349
        rebuildLock.lock();
×
350
        try {
351
            while (rebuilding) {
×
352
                if (!rebuildDone.await(timeout, unit)) {
×
353
                    LOGGER.log(Level.WARNING, "Rebuild did not finish in {0} {1}", new Object[] {timeout, unit});
×
354
                }
355
            }
356
        } finally {
357
            rebuildLock.unlock();
×
358
        }
359
    }
×
360

361
    private Runnable getRebuildRunnable(final SuggesterProjectData data, Progress progress) {
362
        return () -> {
1✔
363
            try {
364
                if (terminating) {
1✔
365
                    return;
×
366
                }
367

368
                Instant start = Instant.now();
1✔
369
                LOGGER.log(Level.FINE, "Rebuilding {0}", data);
1✔
370
                data.rebuild();
1✔
371

372
                Duration d = Duration.between(start, Instant.now());
1✔
373
                LOGGER.log(Level.FINE, "Rebuild of {0} finished, took {1}", new Object[] {data, d});
1✔
374
                progress.increment();
1✔
375
            } catch (Exception e) {
×
376
                LOGGER.log(Level.SEVERE, "Could not rebuild suggester", e);
×
377
            }
1✔
378
        };
1✔
379
    }
380

381
    /**
382
     * Removes the data associated with the provided names.
383
     * @param names names of the indexes to delete
384
     */
385
    public void remove(final Iterable<String> names) {
386
        if (names == null) {
1✔
387
            return;
×
388
        }
389

390
        synchronized (lock) {
1✔
391
            LOGGER.log(Level.INFO, "Removing following suggesters: {0}", names);
1✔
392

393
            for (String suggesterName : names) {
1✔
394
                SuggesterProjectData collection = projectData.get(suggesterName);
1✔
395
                if (collection == null) {
1✔
396
                    LOGGER.log(Level.WARNING, "Unknown suggester {0}", suggesterName);
×
397
                    continue;
×
398
                }
399
                collection.remove();
1✔
400
                projectData.remove(suggesterName);
1✔
401
            }
1✔
402
        }
1✔
403
    }
1✔
404

405
    /**
406
     * Retrieves suggestions based on the specified parameters.
407
     * @param indexReaders index readers with specified name (OpenGrok's project name)
408
     * @param suggesterQuery query for suggestions
409
     * @param query query on which the suggestions depend
410
     * @return suggestions
411
     */
412
    public Suggestions search(
413
            final List<NamedIndexReader> indexReaders,
414
            final SuggesterQuery suggesterQuery,
415
            final Query query
416
    ) {
417
        if (indexReaders == null || suggesterQuery == null) {
1✔
418
            return new Suggestions(Collections.emptyList(), true);
×
419
        }
420

421
        List<NamedIndexReader> readers = indexReaders;
1✔
422
        if (!projectsEnabled) {
1✔
423
            readers = Collections.singletonList(new NamedIndexReader(PROJECTS_DISABLED_KEY,
×
424
                    indexReaders.get(0).getReader()));
×
425
        }
426

427
        Suggestions suggestions;
428
        if (!SuggesterUtils.isComplexQuery(query, suggesterQuery)) { // use WFST for lone prefix
1✔
429
            suggestions = prefixLookup(readers, (SuggesterPrefixQuery) suggesterQuery);
1✔
430
        } else {
431
            suggestions = complexLookup(readers, suggesterQuery, query);
1✔
432
        }
433

434
        return new Suggestions(SuggesterUtils.combineResults(suggestions.items, resultSize),
1✔
435
                suggestions.partialResult);
436
    }
437

438
    private Suggestions prefixLookup(
439
            final List<NamedIndexReader> readers,
440
            final SuggesterPrefixQuery suggesterQuery
441
    ) {
442
        BooleanWrapper partialResult = new BooleanWrapper();
1✔
443

444
        List<LookupResultItem> results = readers.parallelStream().flatMap(namedIndexReader -> {
1✔
445
            SuggesterProjectData data = projectData.get(namedIndexReader.name);
1✔
446
            if (data == null) {
1✔
447
                LOGGER.log(Level.FINE, "{0} not yet initialized", namedIndexReader.name);
×
448
                partialResult.value = true;
×
449
                return Stream.empty();
×
450
            }
451
            boolean gotLock = data.tryLock();
1✔
452
            if (!gotLock) { // do not wait for rebuild
1✔
453
                partialResult.value = true;
×
454
                return Stream.empty();
×
455
            }
456

457
            try {
458
                String prefix = suggesterQuery.getPrefix().text();
1✔
459

460
                return data.lookup(suggesterQuery.getField(), prefix, resultSize)
1✔
461
                        .stream()
1✔
462
                        .map(item -> new LookupResultItem(item.key.toString(), namedIndexReader.name, item.value));
1✔
463
            } finally {
464
                data.unlock();
1✔
465
            }
466
        }).collect(Collectors.toList());
1✔
467

468
        return new Suggestions(results, partialResult.value);
1✔
469
    }
470

471
    private Suggestions complexLookup(
472
            final List<NamedIndexReader> readers,
473
            final SuggesterQuery suggesterQuery,
474
            final Query query
475
    ) {
476
        List<LookupResultItem> results = new ArrayList<>(readers.size() * resultSize);
1✔
477
        List<SuggesterSearchTask> searchTasks = new ArrayList<>(readers.size());
1✔
478
        for (NamedIndexReader ir : readers) {
1✔
479
            searchTasks.add(new SuggesterSearchTask(ir, query, suggesterQuery, results));
1✔
480
        }
1✔
481

482
        List<Future<Void>> futures;
483
        try {
484
            futures = executorService.invokeAll(searchTasks, timeThreshold, TimeUnit.MILLISECONDS);
1✔
485
        } catch (InterruptedException e) {
×
486
            LOGGER.log(Level.WARNING, "Interrupted while invoking suggester search", e);
×
487
            Thread.currentThread().interrupt();
×
488
            return new Suggestions(Collections.emptyList(), true);
×
489
        }
1✔
490

491
        boolean partialResult = futures.stream().anyMatch(Future::isCancelled);
1✔
492

493
        // wait for tasks to finish
494
        for (SuggesterSearchTask searchTask : searchTasks) {
1✔
495
            if (!searchTask.started) {
1✔
496
                continue;
×
497
            }
498

499
            if (!searchTask.finished) {
1✔
500
                synchronized (searchTask) {
×
501
                    while (!searchTask.finished) {
×
502
                        try {
503
                            searchTask.wait();
×
504
                        } catch (InterruptedException e) {
×
505
                            LOGGER.log(Level.WARNING, "Interrupted while waiting for task: {0}", searchTask);
×
506
                            Thread.currentThread().interrupt();
×
507
                        }
×
508
                    }
509
                }
×
510
            }
511
        }
1✔
512
        return new Suggestions(results, partialResult);
1✔
513
    }
514

515
    /**
516
     * Handler for search events.
517
     * @param projects projects that the {@code query} was used to search in
518
     * @param query query that was used to perform the search
519
     */
520
    public void onSearch(final Iterable<String> projects, final Query query) {
521
        if (!allowMostPopular || projects == null) {
1✔
522
            return;
×
523
        }
524
        try {
525
            List<Term> terms = SuggesterUtils.intoTerms(query);
1✔
526

527
            if (!projectsEnabled) {
1✔
528
                incrementSearchCount(terms, PROJECTS_DISABLED_KEY);
×
529
            } else {
530
                projects.forEach(project -> incrementSearchCount(terms, project));
1✔
531
            }
532
        } catch (Exception e) {
×
533
            LOGGER.log(Level.FINE,
×
534
                    String.format("Could not update search count map%s",
×
535
                            projectsEnabled ? " for projects: " + projects : ""), e);
×
536
        }
1✔
537
    }
1✔
538

539
    private void incrementSearchCount(List<Term> terms, final String projectDataKey) {
540
        Optional.ofNullable(projectData.get(projectDataKey))
1✔
541
                .ifPresent(data -> terms.forEach(data::incrementSearchCount));
1✔
542
    }
1✔
543

544
    /**
545
     * Sets the new maximum number of elements the suggester should suggest.
546
     * @param resultSize new number of suggestions to return
547
     */
548
    public void setResultSize(final int resultSize) {
549
        if (resultSize < 0) {
1✔
550
            throw new IllegalArgumentException("Result size cannot be negative");
×
551
        }
552
        this.resultSize = resultSize;
1✔
553
    }
1✔
554

555
    /**
556
     * Sets the new duration for which to await the initialization of the suggester data. Does not affect already
557
     * running initialization.
558
     * @param awaitTerminationTime maximum duration for which to wait for initialization
559
     */
560
    public void setAwaitTerminationTime(final Duration awaitTerminationTime) {
561
        if (awaitTerminationTime.isNegative() || awaitTerminationTime.isZero()) {
1✔
562
            throw new IllegalArgumentException(
×
563
                    "Time to await termination of building the suggester data cannot be 0 or negative");
564
        }
565
        this.awaitTerminationTime = awaitTerminationTime;
1✔
566
    }
1✔
567

568
    /**
569
     * Increases search counts for specific term.
570
     * @param project project where the term resides
571
     * @param term term for which to increase search count
572
     * @param value positive value by which to increase the search count
573
     * @return false if update failed, otherwise true
574
     */
575
    public boolean increaseSearchCount(final String project, final Term term, final int value, final boolean waitForLock) {
576
        if (!allowMostPopular) {
1✔
577
            return false;
×
578
        }
579
        SuggesterProjectData data;
580
        if (!projectsEnabled) {
1✔
581
            data = projectData.get(PROJECTS_DISABLED_KEY);
×
582
        } else {
583
            data = projectData.get(project);
1✔
584
        }
585

586
        if (data == null) {
1✔
587
            LOGGER.log(Level.WARNING, "Cannot update search count because of missing suggester data{}",
×
588
                    projectsEnabled ? " for project " + project : "");
×
589
            return false;
×
590
        }
591

592
        return data.incrementSearchCount(term, value, waitForLock);
1✔
593
    }
594

595
    /**
596
     * Returns the searched terms sorted according to their popularity.
597
     * @param project project for which to return the data
598
     * @param field field for which to return the data
599
     * @param page which page of data to retrieve
600
     * @param pageSize number of results to return
601
     * @return list of terms with their popularity
602
     */
603
    public List<Entry<BytesRef, Integer>> getSearchCounts(
604
            final String project,
605
            final String field,
606
            final int page,
607
            final int pageSize
608
    ) {
609
        SuggesterProjectData data = projectData.get(project);
1✔
610
        if (data == null) {
1✔
611
            LOGGER.log(Level.FINE, "Cannot retrieve search counts because suggester data for project {0} was not found",
1✔
612
                    project);
613
            return Collections.emptyList();
1✔
614
        }
615

616
        return data.getSearchCountsSorted(field, page, pageSize);
1✔
617
    }
618

619
    /**
620
     * Do not allow more suggester rebuilds.
621
     */
622
    public void terminate() {
623
        terminating = true;
×
624
    }
×
625

626
    /**
627
     * Closes opened resources.
628
     */
629
    @Override
630
    public void close() {
631
        executorService.shutdownNow();
1✔
632
        projectData.values().forEach(f -> {
1✔
633
            try {
634
                f.close();
1✔
635
            } catch (IOException e) {
×
636
                LOGGER.log(Level.WARNING, String.format("Could not close suggester data %s", f), e);
×
637
            }
1✔
638
        });
1✔
639
    }
1✔
640

641
    private class SuggesterSearchTask implements Callable<Void> {
642

643
        private final NamedIndexReader namedIndexReader;
644
        private final Query query;
645
        private final SuggesterQuery suggesterQuery;
646
        private final List<LookupResultItem> results;
647

648
        private volatile boolean finished = false;
1✔
649
        private volatile boolean started = false;
1✔
650

651
        SuggesterSearchTask(
652
                final NamedIndexReader namedIndexReader,
653
                final Query query,
654
                final SuggesterQuery suggesterQuery,
655
                final List<LookupResultItem> results
656
        ) {
1✔
657
            this.namedIndexReader = namedIndexReader;
1✔
658
            this.query = query;
1✔
659
            this.suggesterQuery = suggesterQuery;
1✔
660
            this.results = results;
1✔
661
        }
1✔
662

663
        @Override
664
        public Void call() {
665
            try {
666
                started = true;
1✔
667

668
                SuggesterProjectData data = projectData.get(namedIndexReader.name);
1✔
669
                if (data == null) {
1✔
670
                    LOGGER.log(Level.FINE, "{0} not yet initialized", namedIndexReader.name);
×
671
                    return null;
×
672
                }
673
                boolean gotLock = data.tryLock();
1✔
674
                if (!gotLock) { // do not wait for rebuild
1✔
675
                    return null;
×
676
                }
677

678
                try {
679
                    SuggesterSearcher searcher = new SuggesterSearcher(namedIndexReader.reader, resultSize);
1✔
680

681
                    List<LookupResultItem> resultItems = searcher.suggest(query, namedIndexReader.name, suggesterQuery,
1✔
682
                            data.getSearchCounts(suggesterQuery.getField()));
1✔
683

684
                    synchronized (results) {
1✔
685
                        results.addAll(resultItems);
1✔
686
                    }
1✔
687
                } finally {
688
                    data.unlock();
1✔
689
                }
690
            } finally {
691
                synchronized (this) {
1✔
692
                    finished = true;
1✔
693
                    this.notifyAll();
1✔
694
                }
1✔
695
            }
696
            return null;
1✔
697
        }
698
    }
699

700
    /**
701
     * Result suggestions data.
702
     */
703
    public static class Suggestions {
704

705
        private final List<LookupResultItem> items;
706
        private final boolean partialResult;
707

708
        public Suggestions(final List<LookupResultItem> items, final boolean partialResult) {
1✔
709
            this.items = items;
1✔
710
            this.partialResult = partialResult;
1✔
711
        }
1✔
712

713
        public List<LookupResultItem> getItems() {
714
            return items;
1✔
715
        }
716

717
        public boolean isPartialResult() {
718
            return partialResult;
×
719
        }
720
    }
721

722
    /**
723
     * Model classes for holding project name and path to its index directory.
724
     */
725
    public static class NamedIndexDir {
726

727
        /**
728
         * Name of the project.
729
         */
730
        private final String name;
731

732
        /**
733
         * Path to index directory for project with name {@link #name}.
734
         */
735
        private final Path path;
736

737
        public NamedIndexDir(final String name, final Path path) {
1✔
738
            if (name == null) {
1✔
739
                throw new IllegalArgumentException("Name cannot be null");
×
740
            }
741
            if (path == null) {
1✔
742
                throw new IllegalArgumentException("Path cannot be null");
×
743
            }
744

745
            this.name = name;
1✔
746
            this.path = path;
1✔
747
        }
1✔
748

749
        public String getName() {
750
            return name;
×
751
        }
752

753
        public Path getPath() {
754
            return path;
×
755
        }
756

757
        @Override
758
        public String toString() {
759
            return name;
1✔
760
        }
761

762
        @Override
763
        public boolean equals(Object obj) {
764
            if (obj == null) {
1✔
765
                return false;
×
766
            }
767

768
            if (!(obj instanceof NamedIndexDir)) {
1✔
769
                return false;
×
770
            }
771

772
            NamedIndexDir that = (NamedIndexDir) obj;
1✔
773
            return (this.name.equals(that.name) && this.path.equals(that.path));
1✔
774
        }
775

776
        @Override
777
        public int hashCode() {
778
            return Objects.hash(this.name, this.path);
×
779
        }
780
    }
781

782
    /**
783
     * Model class to hold the project name and its {@link IndexReader}.
784
     */
785
    public static class NamedIndexReader {
786

787
        /**
788
         * Name of the project.
789
         */
790
        private final String name;
791

792
        /**
793
         * IndexReader of the project with {@link #name}.
794
         */
795
        private final IndexReader reader;
796

797
        public NamedIndexReader(final String name, final IndexReader reader) {
1✔
798
            if (name == null) {
1✔
799
                throw new IllegalArgumentException("Name cannot be null");
×
800
            }
801
            if (reader == null) {
1✔
802
                throw new IllegalArgumentException("Reader cannot be null");
×
803
            }
804

805
            this.name = name;
1✔
806
            this.reader = reader;
1✔
807
        }
1✔
808

809
        public String getName() {
810
            return name;
×
811
        }
812

813
        public IndexReader getReader() {
814
            return reader;
1✔
815
        }
816

817
        @Override
818
        public String toString() {
819
            return name;
×
820
        }
821

822
    }
823

824
    private static class BooleanWrapper {
825

826
        private volatile boolean value;
827

828
    }
829
}
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