Skip to content

Fix issue #11189 - Implement a caching solution with local storage for citation relations #11845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 59 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3785cb1
Refactor Citations Relations Tab (#11189)
alexandre-cremieux Sep 19, 2024
8048da8
Refactor Citations Relations Tab (#11189)
alexandre-cremieux Sep 24, 2024
18db75e
Refactor Citations Relations Service Layer (#11189)
alexandre-cremieux Sep 28, 2024
0e9de3c
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
koppor Oct 10, 2024
9a31735
Address PR comments (#11901)
alexandre-cremieux Nov 6, 2024
b1133d0
MVStore implementation for citations: first approach (wip for open di…
alexandre-cremieux Nov 11, 2024
6a8b21b
Introduce the DAO layer for relations (#11189):
alexandre-cremieux Nov 17, 2024
01f6da4
MVStoreDAO implementation for citations relations (#11189):
alexandre-cremieux Nov 17, 2024
337780d
Implement a search lock mechanism for citations relations (#11189):
alexandre-cremieux Nov 24, 2024
7c2e32d
Avoid user to force update citation relations even the fetcher return…
alexandre-cremieux Nov 27, 2024
187b5d4
Make the citation relations update automatic after the guard delay is…
alexandre-cremieux Dec 8, 2024
b71d9fc
SearchCitationsRelationsService as singleton
Jan 11, 2025
dda21ed
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Jan 20, 2025
077e9ca
Update failing tests after merge
Jan 20, 2025
0bff913
Clean code style according to checkstyle
Jan 20, 2025
ea36f1a
Clean code style according to checkstyle
Jan 20, 2025
d7f9c2d
Fix pending PR comments
Jan 20, 2025
97b38c6
Fix null pointer exception in CitationsRelationsTab
Jan 22, 2025
54e7e21
Add settings for Citations relations store TTL
Jan 25, 2025
6415b1b
Update MVStoreBibEntryRelationDAO serializer/deserializer
Jan 25, 2025
e18d557
Update CHANGELOG-MD for Citations relations tab caching logic (#11189)
Jan 26, 2025
57b5c02
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Jan 26, 2025
16cfd2c
Return an empty BibEntry from MVStoreBibEntryRelationDAO in case of p…
Jan 26, 2025
33f5cdf
Add a test to ensure that an empty list is returned from in case of s…
Jan 27, 2025
2ebf62e
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Jan 28, 2025
6861ae2
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Jan 31, 2025
393b133
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Feb 2, 2025
e8e223d
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Feb 2, 2025
5727ecb
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
koppor Feb 3, 2025
0cdf495
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Feb 6, 2025
48c48ad
fix typo (#11189)
Feb 6, 2025
7af20c3
Address CHANGE_LOG follwing review (#11189)
Feb 7, 2025
eb3dfd8
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Feb 7, 2025
14ab672
Add comments for tests helpers (#11189)
Feb 7, 2025
061bb89
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Feb 7, 2025
76b2289
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
koppor Feb 9, 2025
a36acd4
Improve BibEntries for test (more confirming to BibTeX)
koppor Feb 9, 2025
8893936
Fix variable name
koppor Feb 9, 2025
37354f5
Streamline code
koppor Feb 9, 2025
b3e1746
Fix class names: "Repository" instead of DAO
koppor Feb 9, 2025
24c1707
Make test BibTeX more consistent to "normal" BibTeX
koppor Feb 9, 2025
d3cac8b
Add comment on test constructor
koppor Feb 9, 2025
deab9b5
Fix class name (not being imperative any more)
koppor Feb 9, 2025
e40637b
Merge branch 'fix-issue-11189-part-00-refactor-citation-relation-tab-…
koppor Feb 9, 2025
543c48b
Fix method name
koppor Feb 9, 2025
a2d3856
Use annotations instead of JavaDoc comments
koppor Feb 9, 2025
31cd171
Add FIXME
koppor Feb 9, 2025
2d31d0f
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
koppor Feb 16, 2025
5bdc850
Add JavaDoc comment
Feb 16, 2025
c03d1f5
Add JavaDoc comment
koppor Feb 16, 2025
4b33bfa
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Apr 27, 2025
d82ab8c
Update CHANGE_LOG
Apr 27, 2025
e9ac90e
Remove the LRU cache layer
Apr 27, 2025
10a1d6a
Simplify repositories naming
Apr 27, 2025
033b296
Open the MVStore once and close it only at application shutdown
Apr 28, 2025
59a2d24
Checkstyle corrections
Apr 28, 2025
4ffd652
Fix MD checks
Apr 28, 2025
7a81e73
Merge branch 'main' into fix-issue-11189-part-00-refactor-citation-re…
Apr 28, 2025
70895c0
Merge remote-tracking branch 'origin/main' into fix-issue-11189-part-…
koppor Jun 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,20 @@
import org.jabref.gui.collab.entrychange.PreviewWithSourceTab;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.entryeditor.EntryEditorTab;
import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher;
import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.NoSelectionModel;
import org.jabref.gui.util.ViewModelListCellFactory;
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.citation.repository.LRUBibEntryRelationsCache;
import org.jabref.logic.citation.repository.LRUBibEntryRelationsRepository;
import org.jabref.logic.citation.service.SearchCitationsRelationsService;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.exporter.BibWriter;
import org.jabref.logic.importer.fetcher.CitationFetcher;
import org.jabref.logic.importer.fetcher.SemanticScholarCitationFetcher;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.os.OS;
import org.jabref.logic.util.BackgroundTask;
Expand Down Expand Up @@ -85,7 +88,7 @@ public class CitationRelationsTab extends EntryEditorTab {
private final GuiPreferences preferences;
private final LibraryTab libraryTab;
private final TaskExecutor taskExecutor;
private final BibEntryRelationsRepository bibEntryRelationsRepository;
private final SearchCitationsRelationsService searchCitationsRelationsService;
private final CitationsRelationsTabViewModel citationsRelationsTabViewModel;
private final DuplicateCheck duplicateCheck;
private final BibEntryTypesManager entryTypesManager;
Expand All @@ -109,9 +112,22 @@ public CitationRelationsTab(DialogService dialogService,

this.entryTypesManager = bibEntryTypesManager;
this.duplicateCheck = new DuplicateCheck(entryTypesManager);
this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()),
new BibEntryRelationsCache());
citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(databaseContext, preferences, undoManager, stateManager, dialogService, fileUpdateMonitor, taskExecutor);
var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository(
new LRUBibEntryRelationsCache()
);
this.searchCitationsRelationsService = new SearchCitationsRelationsService(
new SemanticScholarCitationFetcher(preferences.getImporterPreferences()),
bibEntryRelationsRepository
);
citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(
databaseContext,
preferences,
undoManager,
stateManager,
dialogService,
fileUpdateMonitor,
taskExecutor
);
}

/**
Expand Down Expand Up @@ -405,41 +421,54 @@ private void searchForRelations(BibEntry entry, CheckListView<CitationRelationIt
citedByTask.cancel();
}

BackgroundTask<List<BibEntry>> task;

if (searchType == CitationFetcher.SearchType.CITES) {
task = BackgroundTask.wrap(() -> {
if (shouldRefresh) {
bibEntryRelationsRepository.forceRefreshReferences(entry);
}
return bibEntryRelationsRepository.getReferences(entry);
});
citingTask = task;
} else {
task = BackgroundTask.wrap(() -> {
if (shouldRefresh) {
bibEntryRelationsRepository.forceRefreshCitations(entry);
}
return bibEntryRelationsRepository.getCitations(entry);
});
citedByTask = task;
}

task.onRunning(() -> prepareToSearchForRelations(abortButton, refreshButton, importButton, progress, task))
.onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton,
searchType, importButton, progress, fetchedList, observableList))
this.createBackGroundTask(entry, searchType, shouldRefresh)
.consumeOnRunning(task -> prepareToSearchForRelations(
abortButton, refreshButton, importButton, progress, task
))
.onSuccess(fetchedList -> onSearchForRelationsSucceed(
entry,
listView,
abortButton,
refreshButton,
searchType,
importButton,
progress,
fetchedList,
observableList
))
.onFailure(exception -> {
LOGGER.error("Error while fetching citing Articles", exception);
hideNodes(abortButton, progress, importButton);
listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0",
exception.getMessage())));

listView.setPlaceholder(
new Label(Localization.lang(
"Error while fetching citing entries: %0", exception.getMessage())
)
);
refreshButton.setVisible(true);
dialogService.notify(exception.getMessage());
})
.executeWith(taskExecutor);
}

private BackgroundTask<List<BibEntry>> createBackGroundTask(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Background (without uppercase G)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The highlighted TODO: Which benefts are this - and why should it implemetned lateR?

Copy link
Contributor Author

@alexandre-cremieux alexandre-cremieux Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koppor

This TODO follows a discussion we had earlier that you can read here: #11845 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what stops you from returning a runnable?

If it's used in many places and that makes you rewrite a lot of code to include BackgroundTask constructor, then maybe returning a BackgroundTask right away was not a bad code decision 😃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in theory, this could be needed to keep the level of abstration. Should be decided on where the method is actually used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Callable is expected by the BackgroundTask. Also, Callable has less overhead than a BackgroundTask, it is more convenient to use this type in order to:

  • Ensure that the same task is not already being executed (for same tab for instance) => why kill a task that is already running if we want to minimize the application's footprint ?
  • Insert it into a waiting queue if we want to provide a proper task management logic for this tab (see code of this class => we have one executor per tab instance but the tasks are shared between all those instances...)
  • Decouple the callable implementation from the task itself

As mentioned, this improvement could indeed imply several code changes (in this class):

  • refresh the screen properly => is the user focus on the tab for which a task was ended ?
  • set up a queue would need to override equals and hashCode and so provide a proper implementation of a callable
  • proper testing, etc.

At least, I think we agree that this should not be part of this PR. As I saw that the current implementation could be improved (stopping a task that maybe shouldn't be stopped, avoid same task but different executors for each instances of the tab, etc). I proposed to take a look to this after merging this PR. Reason why the TODO is there (after Oliver's accepted that at that time).

But I can remove completely the TODO also if it bother you (and I understand that cause I do not like TODO also). However, I already have another to issue opened for which I volunteered related to same tab...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just add a link your reasoning in your comment in the to-do?

BibEntry entry, CitationFetcher.SearchType searchType, boolean shouldRefresh
) {
return switch (searchType) {
case CitationFetcher.SearchType.CITES -> {
citingTask = BackgroundTask.wrap(
() -> this.searchCitationsRelationsService.searchReferences(entry, shouldRefresh)
);
yield citingTask;
}
case CitationFetcher.SearchType.CITED_BY -> {
citedByTask = BackgroundTask.wrap(
() -> this.searchCitationsRelationsService.searchCitations(entry, shouldRefresh)
);
yield citedByTask;
}
};
}

private void onSearchForRelationsSucceed(BibEntry entry, CheckListView<CitationRelationItem> listView,
Button abortButton, Button refreshButton,
CitationFetcher.SearchType searchType, Button importButton,
Expand All @@ -456,7 +485,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView<CitationR
.map(localEntry -> new CitationRelationItem(entr, localEntry, true))
.orElseGet(() -> new CitationRelationItem(entr, false)))
.toList()
);
);

if (!observableList.isEmpty()) {
listView.refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher;
import org.jabref.gui.externalfiles.ImportHandler;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.citationkeypattern.CitationKeyGenerator;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.importer.fetcher.CitationFetcher;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jabref.logic.citation.repository;

import java.util.List;

import org.jabref.model.entry.BibEntry;

public interface BibEntryRelationsRepository {

void insertCitations(BibEntry entry, List<BibEntry> citations);

List<BibEntry> readCitations(BibEntry entry);

boolean containsCitations(BibEntry entry);

void insertReferences(BibEntry entry, List<BibEntry> citations);

List<BibEntry> readReferences(BibEntry entry);

boolean containsReferences(BibEntry entry);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.jabref.logic.citation.repository;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.identifier.DOI;

import org.eclipse.jgit.util.LRUMap;

public class LRUBibEntryRelationsCache {
private static final Integer MAX_CACHED_ENTRIES = 100;
private static final Map<DOI, Set<BibEntry>> CITATIONS_MAP = new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES);
private static final Map<DOI, Set<BibEntry>> REFERENCES_MAP = new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES);

public List<BibEntry> getCitations(BibEntry entry) {
return entry
.getDOI()
.stream()
.flatMap(doi -> CITATIONS_MAP.getOrDefault(doi, Set.of()).stream())
.toList();
}

public List<BibEntry> getReferences(BibEntry entry) {
return entry
.getDOI()
.stream()
.flatMap(doi -> REFERENCES_MAP.getOrDefault(doi, Set.of()).stream())
.toList();
}

public void cacheOrMergeCitations(BibEntry entry, List<BibEntry> citations) {
entry.getDOI().ifPresent(doi -> {
var cachedRelations = CITATIONS_MAP.getOrDefault(doi, new LinkedHashSet<>());
cachedRelations.addAll(citations);
CITATIONS_MAP.put(doi, cachedRelations);
});
}

public void cacheOrMergeReferences(BibEntry entry, List<BibEntry> references) {
entry.getDOI().ifPresent(doi -> {
var cachedRelations = REFERENCES_MAP.getOrDefault(doi, new LinkedHashSet<>());
cachedRelations.addAll(references);
REFERENCES_MAP.put(doi, cachedRelations);
});
}

public boolean citationsCached(BibEntry entry) {
return entry.getDOI().map(CITATIONS_MAP::containsKey).orElse(false);
}

public boolean referencesCached(BibEntry entry) {
return entry.getDOI().map(REFERENCES_MAP::containsKey).orElse(false);
}
}
Loading