diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java
index 820e4468c34..c02225e6321 100644
--- a/src/main/java/org/jabref/gui/JabRefGUI.java
+++ b/src/main/java/org/jabref/gui/JabRefGUI.java
@@ -27,6 +27,7 @@
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.UiCommand;
import org.jabref.logic.ai.AiService;
+import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.net.ProxyRegisterer;
import org.jabref.logic.remote.RemotePreferences;
@@ -169,6 +170,11 @@ public void initialize() {
dialogService,
taskExecutor);
Injector.setModelOrService(AiService.class, aiService);
+
+ Injector.setModelOrService(
+ SearchCitationsRelationsService.class,
+ new SearchCitationsRelationsService(preferences.getImporterPreferences())
+ );
}
private void setupProxy() {
diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
index 6a382a7a59e..331a57ee675 100644
--- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
+++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
@@ -50,6 +50,7 @@
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.ai.AiService;
import org.jabref.logic.bibtex.TypedBibEntry;
+import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.help.HelpFile;
import org.jabref.logic.importer.EntryBasedFetcher;
import org.jabref.logic.importer.WebFetchers;
@@ -118,6 +119,7 @@ public class EntryEditor extends BorderPane {
@Inject private KeyBindingRepository keyBindingRepository;
@Inject private JournalAbbreviationRepository journalAbbreviationRepository;
@Inject private AiService aiService;
+ @Inject private SearchCitationsRelationsService searchCitationsRelationsService;
private final List allPossibleTabs;
private final Collection previewTabs;
@@ -296,8 +298,13 @@ private List createTabs() {
tabs.add(new MathSciNetTab());
tabs.add(new FileAnnotationTab(libraryTab.getAnnotationCache()));
tabs.add(new SciteTab(preferences, taskExecutor, dialogService));
- tabs.add(new CitationRelationsTab(dialogService, databaseContext,
- undoManager, stateManager, fileMonitor, preferences, libraryTab, taskExecutor, bibEntryTypesManager));
+ tabs.add(new CitationRelationsTab(
+ dialogService, databaseContext,
+ undoManager, stateManager,
+ fileMonitor, preferences,
+ libraryTab, taskExecutor,
+ bibEntryTypesManager, searchCitationsRelationsService
+ ));
tabs.add(new RelatedArticlesTab(buildInfo, databaseContext, preferences, dialogService, taskExecutor));
sourceTab = new SourceTab(
databaseContext,
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java
deleted file mode 100644
index 55888aa660f..00000000000
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.jabref.gui.entryeditor.citationrelationtab;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import org.jabref.model.entry.BibEntry;
-import org.jabref.model.entry.identifier.DOI;
-
-import org.eclipse.jgit.util.LRUMap;
-
-public class BibEntryRelationsCache {
- private static final Integer MAX_CACHED_ENTRIES = 100;
- private static final Map> CITATIONS_MAP = new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES);
- private static final Map> REFERENCES_MAP = new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES);
-
- public List getCitations(BibEntry entry) {
- return CITATIONS_MAP.getOrDefault(entry.getDOI().map(DOI::getDOI).orElse(""), Collections.emptyList());
- }
-
- public List getReferences(BibEntry entry) {
- return REFERENCES_MAP.getOrDefault(entry.getDOI().map(DOI::getDOI).orElse(""), Collections.emptyList());
- }
-
- public void cacheOrMergeCitations(BibEntry entry, List citations) {
- entry.getDOI().ifPresent(doi -> CITATIONS_MAP.put(doi.getDOI(), citations));
- }
-
- public void cacheOrMergeReferences(BibEntry entry, List references) {
- entry.getDOI().ifPresent(doi -> REFERENCES_MAP.putIfAbsent(doi.getDOI(), references));
- }
-
- public boolean citationsCached(BibEntry entry) {
- return CITATIONS_MAP.containsKey(entry.getDOI().map(DOI::getDOI).orElse(""));
- }
-
- public boolean referencesCached(BibEntry entry) {
- return REFERENCES_MAP.containsKey(entry.getDOI().map(DOI::getDOI).orElse(""));
- }
-}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java
deleted file mode 100644
index f7f29052da2..00000000000
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package org.jabref.gui.entryeditor.citationrelationtab;
-
-import java.util.List;
-
-import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher;
-import org.jabref.logic.importer.FetcherException;
-import org.jabref.model.entry.BibEntry;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class BibEntryRelationsRepository {
- private static final Logger LOGGER = LoggerFactory.getLogger(BibEntryRelationsRepository.class);
-
- private final SemanticScholarFetcher fetcher;
- private final BibEntryRelationsCache cache;
-
- public BibEntryRelationsRepository(SemanticScholarFetcher fetcher, BibEntryRelationsCache cache) {
- this.fetcher = fetcher;
- this.cache = cache;
- }
-
- public List getCitations(BibEntry entry) {
- if (needToRefreshCitations(entry)) {
- forceRefreshCitations(entry);
- }
-
- return cache.getCitations(entry);
- }
-
- public List getReferences(BibEntry entry) {
- if (needToRefreshReferences(entry)) {
- List references;
- try {
- references = fetcher.searchCiting(entry);
- } catch (FetcherException e) {
- LOGGER.error("Error while fetching references", e);
- references = List.of();
- }
- cache.cacheOrMergeReferences(entry, references);
- }
-
- return cache.getReferences(entry);
- }
-
- public void forceRefreshCitations(BibEntry entry) {
- try {
- List citations = fetcher.searchCitedBy(entry);
- cache.cacheOrMergeCitations(entry, citations);
- } catch (FetcherException e) {
- LOGGER.error("Error while fetching citations", e);
- }
- }
-
- public boolean needToRefreshCitations(BibEntry entry) {
- return !cache.citationsCached(entry);
- }
-
- public boolean needToRefreshReferences(BibEntry entry) {
- return !cache.referencesCached(entry);
- }
-
- public void forceRefreshReferences(BibEntry entry) {
- List references;
- try {
- references = fetcher.searchCiting(entry);
- } catch (FetcherException e) {
- LOGGER.error("Error while fetching references", e);
- references = List.of();
- }
- cache.cacheOrMergeReferences(entry, references);
- }
-}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
index 3ab2a167b9c..e267b856adb 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
@@ -36,8 +36,6 @@
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;
@@ -45,8 +43,10 @@
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.bibtex.FieldWriter;
+import org.jabref.logic.citation.SearchCitationsRelationsService;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.exporter.BibWriter;
+import org.jabref.logic.importer.fetcher.CitationFetcher;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.os.OS;
import org.jabref.logic.util.BackgroundTask;
@@ -85,7 +85,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;
@@ -98,7 +98,8 @@ public CitationRelationsTab(DialogService dialogService,
GuiPreferences preferences,
LibraryTab libraryTab,
TaskExecutor taskExecutor,
- BibEntryTypesManager bibEntryTypesManager) {
+ BibEntryTypesManager bibEntryTypesManager,
+ SearchCitationsRelationsService searchCitationsRelationsService) {
this.dialogService = dialogService;
this.databaseContext = databaseContext;
this.preferences = preferences;
@@ -109,9 +110,17 @@ 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);
+ this.searchCitationsRelationsService = searchCitationsRelationsService;
+
+ citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(
+ databaseContext,
+ preferences,
+ undoManager,
+ stateManager,
+ dialogService,
+ fileUpdateMonitor,
+ taskExecutor
+ );
}
/**
@@ -187,11 +196,11 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) {
refreshCitingButton.setOnMouseClicked(event -> {
searchForRelations(entry, citingListView, abortCitingButton,
- refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true);
+ refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress);
});
refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton,
- refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true));
+ refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress));
// Create SplitPane to hold all nodes above
SplitPane container = new SplitPane(citingVBox, citedByVBox);
@@ -199,10 +208,10 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) {
styleFetchedListView(citingListView);
searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton,
- CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false);
+ CitationFetcher.SearchType.CITES, importCitingButton, citingProgress);
searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton,
- CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false);
+ CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress);
return container;
}
@@ -385,7 +394,7 @@ protected void bindToEntry(BibEntry entry) {
*/
private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton,
Button refreshButton, CitationFetcher.SearchType searchType, Button importButton,
- ProgressIndicator progress, boolean shouldRefresh) {
+ ProgressIndicator progress) {
if (entry.getDOI().isEmpty()) {
hideNodes(abortButton, progress);
showNodes(refreshButton);
@@ -399,47 +408,61 @@ private void searchForRelations(BibEntry entry, CheckListView> 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)
+ .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())));
-
refreshButton.setVisible(true);
dialogService.notify(exception.getMessage());
})
.executeWith(taskExecutor);
}
+ /**
+ * TODO: Make the method return a callable and let the calling method create the background task.
+ */
+ private BackgroundTask> createBackGroundTask(
+ BibEntry entry, CitationFetcher.SearchType searchType
+ ) {
+ return switch (searchType) {
+ case CitationFetcher.SearchType.CITES -> {
+ citingTask = BackgroundTask.wrap(
+ () -> this.searchCitationsRelationsService.searchReferences(entry)
+ );
+ yield citingTask;
+ }
+ case CitationFetcher.SearchType.CITED_BY -> {
+ citedByTask = BackgroundTask.wrap(
+ () -> this.searchCitationsRelationsService.searchCitations(entry)
+ );
+ yield citedByTask;
+ }
+ };
+ }
+
private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView,
Button abortButton, Button refreshButton,
CitationFetcher.SearchType searchType, Button importButton,
@@ -456,7 +479,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView new CitationRelationItem(entr, localEntry, true))
.orElseGet(() -> new CitationRelationItem(entr, false)))
.toList()
- );
+ );
if (!observableList.isEmpty()) {
listView.refresh();
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
index 7ae52965b8f..f095e08e45e 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java
@@ -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;
diff --git a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java
new file mode 100644
index 00000000000..07c5b3fb3bf
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java
@@ -0,0 +1,66 @@
+package org.jabref.logic.citation;
+
+import java.util.List;
+
+import org.jabref.logic.citation.repository.BibEntryRelationsRepository;
+import org.jabref.logic.citation.repository.ChainBibEntryRelationsRepository;
+import org.jabref.logic.importer.FetcherException;
+import org.jabref.logic.importer.ImporterPreferences;
+import org.jabref.logic.importer.fetcher.CitationFetcher;
+import org.jabref.logic.importer.fetcher.SemanticScholarCitationFetcher;
+import org.jabref.logic.util.Directories;
+import org.jabref.model.entry.BibEntry;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SearchCitationsRelationsService {
+
+ private static final Logger LOGGER = LoggerFactory
+ .getLogger(SearchCitationsRelationsService.class);
+
+ private final CitationFetcher citationFetcher;
+ private final BibEntryRelationsRepository relationsRepository;
+
+ public SearchCitationsRelationsService(
+ CitationFetcher citationFetcher, BibEntryRelationsRepository repository
+ ) {
+ this.citationFetcher = citationFetcher;
+ this.relationsRepository = repository;
+ }
+
+ public SearchCitationsRelationsService(ImporterPreferences importerPreferences) {
+ this.citationFetcher = new SemanticScholarCitationFetcher(importerPreferences);
+ this.relationsRepository = ChainBibEntryRelationsRepository.of(
+ Directories.getCitationsRelationsDirectory()
+ );
+ }
+
+ public List searchReferences(BibEntry referencer) {
+ boolean isFetchingAllowed = this.relationsRepository.isReferencesUpdatable(referencer)
+ || !this.relationsRepository.containsReferences(referencer);
+ if (isFetchingAllowed) {
+ try {
+ var references = this.citationFetcher.searchCiting(referencer);
+ this.relationsRepository.insertReferences(referencer, references);
+ } catch (FetcherException e) {
+ LOGGER.error("Error while fetching references for entry {}", referencer.getTitle(), e);
+ }
+ }
+ return this.relationsRepository.readReferences(referencer);
+ }
+
+ public List searchCitations(BibEntry cited) {
+ boolean isFetchingAllowed = this.relationsRepository.isCitationsUpdatable(cited)
+ || !this.relationsRepository.containsCitations(cited);
+ if (isFetchingAllowed) {
+ try {
+ var citations = this.citationFetcher.searchCitedBy(cited);
+ this.relationsRepository.insertCitations(cited, citations);
+ } catch (FetcherException e) {
+ LOGGER.error("Error while fetching citations for entry {}", cited.getTitle(), e);
+ }
+ }
+ return this.relationsRepository.readCitations(cited);
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java
new file mode 100644
index 00000000000..883a4566a3c
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java
@@ -0,0 +1,18 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+
+import org.jabref.model.entry.BibEntry;
+
+public interface BibEntryRelationDAO {
+
+ List getRelations(BibEntry entry);
+
+ void cacheOrMergeRelations(BibEntry entry, List relations);
+
+ boolean containsKey(BibEntry entry);
+
+ default boolean isUpdatable(BibEntry entry) {
+ return true;
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java
new file mode 100644
index 00000000000..2445e13a6fc
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java
@@ -0,0 +1,59 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+
+import org.jabref.model.entry.BibEntry;
+
+public class BibEntryRelationDAOChain implements BibEntryRelationDAO {
+
+ private static final BibEntryRelationDAO EMPTY = new BibEntryRelationDAOChain(null, null);
+
+ private final BibEntryRelationDAO current;
+ private final BibEntryRelationDAO next;
+
+ BibEntryRelationDAOChain(BibEntryRelationDAO current, BibEntryRelationDAO next) {
+ this.current = current;
+ this.next = next;
+ }
+
+ @Override
+ public List getRelations(BibEntry entry) {
+ if (this.current.containsKey(entry)) {
+ return this.current.getRelations(entry);
+ }
+ if (this.next == EMPTY) {
+ return List.of();
+ }
+ var relations = this.next.getRelations(entry);
+ this.current.cacheOrMergeRelations(entry, relations);
+ // Makes sure to obtain a copy and not a direct reference to what was inserted
+ return this.current.getRelations(entry);
+ }
+
+ @Override
+ public void cacheOrMergeRelations(BibEntry entry, List relations) {
+ if (this.next != EMPTY) {
+ this.next.cacheOrMergeRelations(entry, relations);
+ }
+ this.current.cacheOrMergeRelations(entry, relations);
+ }
+
+ @Override
+ public boolean containsKey(BibEntry entry) {
+ return this.current.containsKey(entry)
+ || (this.next != EMPTY && this.next.containsKey(entry));
+ }
+
+ @Override
+ public boolean isUpdatable(BibEntry entry) {
+ return this.current.isUpdatable(entry)
+ && (this.next == EMPTY || this.next.isUpdatable(entry));
+ }
+
+ public static BibEntryRelationDAO of(BibEntryRelationDAO... dao) {
+ return List.of(dao)
+ .reversed()
+ .stream()
+ .reduce(EMPTY, (acc, current) -> new BibEntryRelationDAOChain(current, acc));
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java
new file mode 100644
index 00000000000..cb99b791cad
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java
@@ -0,0 +1,24 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+
+import org.jabref.model.entry.BibEntry;
+
+public interface BibEntryRelationsRepository {
+
+ void insertCitations(BibEntry entry, List citations);
+
+ List readCitations(BibEntry entry);
+
+ boolean containsCitations(BibEntry entry);
+
+ boolean isCitationsUpdatable(BibEntry entry);
+
+ void insertReferences(BibEntry entry, List citations);
+
+ List readReferences(BibEntry entry);
+
+ boolean containsReferences(BibEntry entry);
+
+ boolean isReferencesUpdatable(BibEntry entry);
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java
new file mode 100644
index 00000000000..cdad83f7f52
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java
@@ -0,0 +1,77 @@
+package org.jabref.logic.citation.repository;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+
+import org.jabref.model.entry.BibEntry;
+
+public class ChainBibEntryRelationsRepository implements BibEntryRelationsRepository {
+
+ private static final String CITATIONS_STORE = "citations";
+ private static final String REFERENCES_STORE = "references";
+
+ private final BibEntryRelationDAO citationsDao;
+ private final BibEntryRelationDAO referencesDao;
+
+ public ChainBibEntryRelationsRepository(Path citationsStore, Path relationsStore) {
+ this.citationsDao = BibEntryRelationDAOChain.of(
+ LRUCacheBibEntryRelationsDAO.CITATIONS,
+ new MVStoreBibEntryRelationDAO(citationsStore, CITATIONS_STORE)
+ );
+ this.referencesDao = BibEntryRelationDAOChain.of(
+ LRUCacheBibEntryRelationsDAO.REFERENCES,
+ new MVStoreBibEntryRelationDAO(relationsStore, REFERENCES_STORE)
+ );
+ }
+
+ @Override
+ public void insertCitations(BibEntry entry, List citations) {
+ citationsDao.cacheOrMergeRelations(
+ entry, Objects.requireNonNullElseGet(citations, List::of)
+ );
+ }
+
+ @Override
+ public List readCitations(BibEntry entry) {
+ return citationsDao.getRelations(entry);
+ }
+
+ @Override
+ public boolean containsCitations(BibEntry entry) {
+ return citationsDao.containsKey(entry);
+ }
+
+ @Override
+ public boolean isCitationsUpdatable(BibEntry entry) {
+ return citationsDao.isUpdatable(entry);
+ }
+
+ @Override
+ public void insertReferences(BibEntry entry, List references) {
+ referencesDao.cacheOrMergeRelations(
+ entry, Objects.requireNonNullElseGet(references, List::of)
+ );
+ }
+
+ @Override
+ public List readReferences(BibEntry entry) {
+ return referencesDao.getRelations(entry);
+ }
+
+ @Override
+ public boolean containsReferences(BibEntry entry) {
+ return referencesDao.containsKey(entry);
+ }
+
+ @Override
+ public boolean isReferencesUpdatable(BibEntry entry) {
+ return referencesDao.isUpdatable(entry);
+ }
+
+ public static ChainBibEntryRelationsRepository of(Path citationsRelationsDirectory) {
+ var citationsPath = citationsRelationsDirectory.resolve("%s.mv".formatted(CITATIONS_STORE));
+ var relationsPath = citationsRelationsDirectory.resolve("%s.mv".formatted(REFERENCES_STORE));
+ return new ChainBibEntryRelationsRepository(citationsPath, relationsPath);
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java
new file mode 100644
index 00000000000..3b753dc92c0
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java
@@ -0,0 +1,56 @@
+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;
+
+import static org.jabref.logic.citation.repository.LRUCacheBibEntryRelationsDAO.Configuration.MAX_CACHED_ENTRIES;
+
+public enum LRUCacheBibEntryRelationsDAO implements BibEntryRelationDAO {
+
+ CITATIONS(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)),
+ REFERENCES(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES));
+
+ public static class Configuration {
+ public static final int MAX_CACHED_ENTRIES = 128; // Let's use a power of two for sizing
+ }
+
+ private final Map> relationsMap;
+
+ LRUCacheBibEntryRelationsDAO(Map> relationsMap) {
+ this.relationsMap = relationsMap;
+ }
+
+ @Override
+ public List getRelations(BibEntry entry) {
+ return entry
+ .getDOI()
+ .stream()
+ .flatMap(doi -> this.relationsMap.getOrDefault(doi, Set.of()).stream())
+ .toList();
+ }
+
+ @Override
+ public synchronized void cacheOrMergeRelations(BibEntry entry, List relations) {
+ entry.getDOI().ifPresent(doi -> {
+ var cachedRelations = this.relationsMap.getOrDefault(doi, new LinkedHashSet<>());
+ cachedRelations.addAll(relations);
+ relationsMap.put(doi, cachedRelations);
+ });
+ }
+
+ @Override
+ public boolean containsKey(BibEntry entry) {
+ return entry.getDOI().map(this.relationsMap::containsKey).orElse(false);
+ }
+
+ public void clearEntries() {
+ this.relationsMap.clear();
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java
new file mode 100644
index 00000000000..492e31df016
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java
@@ -0,0 +1,255 @@
+package org.jabref.logic.citation.repository;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.ZoneId;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.time.LocalDateTime;
+
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.identifier.DOI;
+import org.jabref.model.entry.types.StandardEntryType;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.h2.mvstore.MVMap;
+import org.h2.mvstore.MVStore;
+import org.h2.mvstore.WriteBuffer;
+import org.h2.mvstore.type.BasicDataType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MVStoreBibEntryRelationDAO implements BibEntryRelationDAO {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MVStoreBibEntryRelationDAO.class);
+ private final static ZoneId TIME_STAMP_ZONE_ID = ZoneId.of("UTC");
+
+ private final String mapName;
+ private final String insertionTimeStampMapName;
+ private final MVStore.Builder storeConfiguration;
+ private final MVMap.Builder> mapConfiguration =
+ new MVMap.Builder>().valueType(new BibEntryHashSetSerializer());
+
+ MVStoreBibEntryRelationDAO(Path path, String mapName) {
+
+ try {
+ if (!Files.exists(path.getParent())) {
+ Files.createDirectories(path.getParent());
+ }
+ if (!Files.exists(path)) {
+ Files.createFile(path);
+ }
+ } catch (IOException e) {
+ LOGGER.error("An error occurred while opening {} storage", mapName, e);
+ }
+
+ this.mapName = mapName;
+ this.insertionTimeStampMapName = mapName + "-insertion-timestamp";
+ this.storeConfiguration = new MVStore.Builder().autoCommitDisabled().fileName(path.toAbsolutePath().toString());
+ }
+
+ @Override
+ public List getRelations(BibEntry entry) {
+ return entry
+ .getDOI()
+ .map(doi -> {
+ try (var store = this.storeConfiguration.open()) {
+ MVMap> relationsMap = store.openMap(mapName, mapConfiguration);
+ return relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>()).stream().toList();
+ }
+ })
+ .orElse(List.of());
+ }
+
+ /**
+ * Allows insertion of empty list in order to keep track of insertion date for an entry.
+ *
+ * @param entry should not be null
+ * @param relations should not be null
+ */
+ @Override
+ synchronized public void cacheOrMergeRelations(BibEntry entry, List relations) {
+ entry.getDOI().ifPresent(doi -> {
+ try (var store = this.storeConfiguration.open()) {
+ // Save the relations
+ MVMap> relationsMap = store.openMap(mapName, mapConfiguration);
+ var relationsAlreadyStored = relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>());
+ relationsAlreadyStored.addAll(relations);
+ relationsMap.put(doi.getDOI(), relationsAlreadyStored);
+
+ // Save insertion timestamp
+ var insertionTime = LocalDateTime.now(TIME_STAMP_ZONE_ID);
+ MVMap insertionTimeStampMap = store.openMap(insertionTimeStampMapName);
+ insertionTimeStampMap.put(doi.getDOI(), insertionTime);
+
+ // Commit
+ store.commit();
+ }
+ });
+ }
+
+ @Override
+ synchronized public boolean containsKey(BibEntry entry) {
+ return entry
+ .getDOI()
+ .map(doi -> {
+ try (var store = this.storeConfiguration.open()) {
+ MVMap> relationsMap = store.openMap(mapName, mapConfiguration);
+ return relationsMap.containsKey(doi.getDOI());
+ }
+ })
+ .orElse(false);
+ }
+
+ @Override
+ synchronized public boolean isUpdatable(BibEntry entry) {
+ var clock = Clock.system(TIME_STAMP_ZONE_ID);
+ return this.isUpdatable(entry, clock);
+ }
+
+ @VisibleForTesting
+ boolean isUpdatable(final BibEntry entry, final Clock clock) {
+ final var executionTime = LocalDateTime.now(clock);
+ return entry
+ .getDOI()
+ .map(doi -> {
+ try (var store = this.storeConfiguration.open()) {
+ MVMap insertionTimeStampMap = store.openMap(insertionTimeStampMapName);
+ return insertionTimeStampMap.getOrDefault(doi.getDOI(), executionTime);
+ }
+ })
+ .map(lastExecutionTime ->
+ lastExecutionTime.equals(executionTime) || lastExecutionTime.isBefore(executionTime.minusWeeks(1))
+ )
+ .orElse(true);
+ }
+
+ private static class BibEntrySerializer extends BasicDataType {
+
+ private final static String FIELD_SEPARATOR = "--";
+
+ private static String toString(BibEntry entry) {
+ return String.join(
+ FIELD_SEPARATOR,
+ entry.getTitle().orElse("null"),
+ entry.getField(StandardField.YEAR).orElse("null"),
+ entry.getField(StandardField.AUTHOR).orElse("null"),
+ entry.getType().getDisplayName() == null ? "null" : entry.getType().getDisplayName(),
+ entry.getDOI().map(DOI::getDOI).orElse("null"),
+ entry.getField(StandardField.URL).orElse("null"),
+ entry.getField(StandardField.ABSTRACT).orElse("null")
+ );
+ }
+
+ private static Optional extractFieldValue(String field) {
+ return Objects.equals(field, "null") || field == null
+ ? Optional.empty()
+ : Optional.of(field);
+ }
+
+ private static BibEntry fromString(String serializedString) {
+ var fields = serializedString.split(FIELD_SEPARATOR);
+ BibEntry entry = new BibEntry();
+ extractFieldValue(fields[0]).ifPresent(title -> entry.setField(StandardField.TITLE, title));
+ extractFieldValue(fields[1]).ifPresent(year -> entry.setField(StandardField.YEAR, year));
+ extractFieldValue(fields[2]).ifPresent(authors -> entry.setField(StandardField.AUTHOR, authors));
+ extractFieldValue(fields[3]).ifPresent(type -> entry.setType(StandardEntryType.valueOf(type)));
+ extractFieldValue(fields[4]).ifPresent(doi -> entry.setField(StandardField.DOI, doi));
+ extractFieldValue(fields[5]).ifPresent(url -> entry.setField(StandardField.URL, url));
+ extractFieldValue(fields[6])
+ .ifPresent(entryAbstract -> entry.setField(StandardField.ABSTRACT, entryAbstract));
+ return entry;
+ }
+
+ @Override
+ public int getMemory(BibEntry obj) {
+ return toString(obj).getBytes(StandardCharsets.UTF_8).length;
+ }
+
+ @Override
+ public void write(WriteBuffer buff, BibEntry bibEntry) {
+ var asBytes = toString(bibEntry).getBytes(StandardCharsets.UTF_8);
+ buff.putInt(asBytes.length);
+ buff.put(asBytes);
+ }
+
+ @Override
+ public BibEntry read(ByteBuffer buff) {
+ int serializedEntrySize = buff.getInt();
+ var serializedEntry = new byte[serializedEntrySize];
+ buff.get(serializedEntry);
+ return fromString(new String(serializedEntry, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public int compare(BibEntry a, BibEntry b) {
+ if (a == null || b == null) {
+ throw new NullPointerException();
+ }
+ return toString(a).compareTo(toString(b));
+ }
+
+ @Override
+ public BibEntry[] createStorage(int size) {
+ return new BibEntry[size];
+ }
+
+ @Override
+ public boolean isMemoryEstimationAllowed() {
+ return false;
+ }
+ }
+
+ private static class BibEntryHashSetSerializer extends BasicDataType> {
+
+ private final BasicDataType bibEntryDataType = new BibEntrySerializer();
+
+ /**
+ * Memory size is the sum of all aggregated bibEntries' memory size plus 4 bytes.
+ * Those 4 bytes are used to store the length of the collection itself.
+ *
+ * @param bibEntries should not be null
+ * @return total size in memory of the serialized collection of bib entries
+ */
+ @Override
+ public int getMemory(LinkedHashSet bibEntries) {
+ return bibEntries
+ .stream()
+ .map(this.bibEntryDataType::getMemory)
+ .reduce(0, Integer::sum) + 4;
+ }
+
+ @Override
+ public void write(WriteBuffer buff, LinkedHashSet bibEntries) {
+ buff.putInt(bibEntries.size());
+ bibEntries.forEach(entry -> this.bibEntryDataType.write(buff, entry));
+ }
+
+ @Override
+ public LinkedHashSet read(ByteBuffer buff) {
+ return IntStream.range(0, buff.getInt())
+ .mapToObj(it -> this.bibEntryDataType.read(buff))
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public LinkedHashSet[] createStorage(int size) {
+ return new LinkedHashSet[size];
+ }
+
+ @Override
+ public boolean isMemoryEstimationAllowed() {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/CitationFetcher.java
similarity index 94%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java
rename to src/main/java/org/jabref/logic/importer/fetcher/CitationFetcher.java
index 1b87c7ab0bb..58c4f32d080 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java
+++ b/src/main/java/org/jabref/logic/importer/fetcher/CitationFetcher.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.logic.importer.fetcher;
import java.util.List;
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java
similarity index 90%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java
rename to src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java
index 557a135741e..5efc66255ee 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java
+++ b/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.logic.importer.fetcher;
import java.net.MalformedURLException;
import java.net.URI;
@@ -7,21 +7,22 @@
import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.ImporterPreferences;
-import org.jabref.logic.importer.fetcher.CustomizableKeyFetcher;
import org.jabref.logic.net.URLDownload;
import org.jabref.logic.util.BuildInfo;
+import org.jabref.model.citation.semanticscholar.CitationsResponse;
+import org.jabref.model.citation.semanticscholar.ReferencesResponse;
import org.jabref.model.entry.BibEntry;
import com.google.gson.Gson;
-public class SemanticScholarFetcher implements CitationFetcher, CustomizableKeyFetcher {
+public class SemanticScholarCitationFetcher implements CitationFetcher, CustomizableKeyFetcher {
private static final String SEMANTIC_SCHOLAR_API = "https://api.semanticscholar.org/graph/v1/";
private static final String API_KEY = new BuildInfo().semanticScholarApiKey;
private final ImporterPreferences importerPreferences;
- public SemanticScholarFetcher(ImporterPreferences importerPreferences) {
+ public SemanticScholarCitationFetcher(ImporterPreferences importerPreferences) {
this.importerPreferences = importerPreferences;
}
diff --git a/src/main/java/org/jabref/logic/util/BackgroundTask.java b/src/main/java/org/jabref/logic/util/BackgroundTask.java
index 1a905432945..11e2b4083e4 100644
--- a/src/main/java/org/jabref/logic/util/BackgroundTask.java
+++ b/src/main/java/org/jabref/logic/util/BackgroundTask.java
@@ -172,6 +172,16 @@ public BackgroundTask onRunning(Runnable onRunning) {
return this;
}
+ /**
+ * Curry a consumer to on an on running runnable and invoke it after the task is started.
+ *
+ * @param onRunningConsumer should not be null
+ * @see BackgroundTask#consumeOnRunning(Consumer)
+ */
+ public BackgroundTask consumeOnRunning(Consumer> onRunningConsumer) {
+ return this.onRunning(() -> onRunningConsumer.accept(this));
+ }
+
/**
* Sets the {@link Consumer} that is invoked after the task is successfully finished.
* The consumer always runs on the JavaFX thread.
diff --git a/src/main/java/org/jabref/logic/util/Directories.java b/src/main/java/org/jabref/logic/util/Directories.java
index 00396975da7..5808fd0d6b3 100644
--- a/src/main/java/org/jabref/logic/util/Directories.java
+++ b/src/main/java/org/jabref/logic/util/Directories.java
@@ -62,4 +62,13 @@ public static Path getSslDirectory() {
"ssl",
OS.APP_DIR_APP_AUTHOR));
}
+
+ public static Path getCitationsRelationsDirectory() {
+ return Path.of(
+ AppDirsFactory.getInstance()
+ .getUserDataDir(
+ OS.APP_DIR_APP_NAME,
+ "relations",
+ OS.APP_DIR_APP_AUTHOR));
+ }
}
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java b/src/main/java/org/jabref/model/citation/semanticscholar/AuthorResponse.java
similarity index 84%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/AuthorResponse.java
index 539b99cc39d..8489099a4fb 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/AuthorResponse.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
/**
* Used for GSON
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java b/src/main/java/org/jabref/model/citation/semanticscholar/CitationDataItem.java
similarity index 79%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/CitationDataItem.java
index 684285b46df..8f9d44535e9 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/CitationDataItem.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
/**
* Used for GSON
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java b/src/main/java/org/jabref/model/citation/semanticscholar/CitationsResponse.java
similarity index 89%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/CitationsResponse.java
index 999eb7eca2a..8fdbec26948 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/CitationsResponse.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
import java.util.List;
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java b/src/main/java/org/jabref/model/citation/semanticscholar/PaperDetails.java
similarity index 98%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/PaperDetails.java
index 58ba269616e..073e15f384d 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/PaperDetails.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/PaperDetails.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
import java.util.List;
import java.util.Map;
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java b/src/main/java/org/jabref/model/citation/semanticscholar/ReferenceDataItem.java
similarity index 70%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/ReferenceDataItem.java
index b9c53c355e9..ccbb170355c 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/ReferenceDataItem.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
/**
* Used for GSON
diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java b/src/main/java/org/jabref/model/citation/semanticscholar/ReferencesResponse.java
similarity index 89%
rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java
rename to src/main/java/org/jabref/model/citation/semanticscholar/ReferencesResponse.java
index 0a6ac34af07..a0f9c6426a3 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java
+++ b/src/main/java/org/jabref/model/citation/semanticscholar/ReferencesResponse.java
@@ -1,4 +1,4 @@
-package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar;
+package org.jabref.model.citation.semanticscholar;
import java.util.List;
diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java
index f0d2d43689e..0a9c31f4f0f 100644
--- a/src/main/java/org/jabref/model/entry/BibEntry.java
+++ b/src/main/java/org/jabref/model/entry/BibEntry.java
@@ -1,5 +1,6 @@
package org.jabref.model.entry;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -93,7 +94,7 @@
*
*/
@AllowedToUseLogic("because it needs access to parser and writers")
-public class BibEntry implements Cloneable {
+public class BibEntry implements Cloneable, Serializable {
public static final EntryType DEFAULT_TYPE = StandardEntryType.Misc;
private static final Logger LOGGER = LoggerFactory.getLogger(BibEntry.class);
@@ -995,6 +996,11 @@ public BibEntry withMonth(Month parsedMonth) {
return this;
}
+ public BibEntry withType(EntryType type) {
+ this.setType(type);
+ return this;
+ }
+
/*
* Returns user comments (arbitrary text before the entry), if they exist. If not, returns the empty String
*/
diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepositoryTest.java
deleted file mode 100644
index 41106d57a6b..00000000000
--- a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepositoryTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.jabref.gui.entryeditor.citationrelationtab;
-
-import java.util.List;
-
-import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher;
-import org.jabref.model.entry.BibEntry;
-import org.jabref.model.entry.field.StandardField;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-class BibEntryRelationsRepositoryTest {
-
- private List getCitedBy(BibEntry entry) {
- return List.of(createCitingBibEntry(entry));
- }
-
- private BibEntry createBibEntry(int i) {
- return new BibEntry()
- .withCitationKey("entry" + i)
- .withField(StandardField.DOI, "10.1234/5678" + i);
- }
-
- private BibEntry createCitingBibEntry(Integer i) {
- return new BibEntry()
- .withCitationKey("citing_entry" + i)
- .withField(StandardField.DOI, "10.2345/6789" + i);
- }
-
- private BibEntry createCitingBibEntry(BibEntry citedEntry) {
- return createCitingBibEntry(Integer.valueOf(citedEntry.getCitationKey().get().substring(5)));
- }
-
- @Test
- void getCitations() throws Exception {
- SemanticScholarFetcher semanticScholarFetcher = mock(SemanticScholarFetcher.class);
- when(semanticScholarFetcher.searchCitedBy(any(BibEntry.class))).thenAnswer(invocation -> {
- BibEntry entry = invocation.getArgument(0);
- return getCitedBy(entry);
- });
- BibEntryRelationsCache bibEntryRelationsCache = new BibEntryRelationsCache();
-
- BibEntryRelationsRepository bibEntryRelationsRepository = new BibEntryRelationsRepository(semanticScholarFetcher, bibEntryRelationsCache);
-
- for (int i = 0; i < 150; i++) {
- BibEntry entry = createBibEntry(i);
- List citations = bibEntryRelationsRepository.getCitations(entry);
- assertEquals(getCitedBy(entry), citations);
- }
-
- for (int i = 0; i < 150; i++) {
- BibEntry entry = createBibEntry(i);
- List citations = bibEntryRelationsRepository.getCitations(entry);
- assertEquals(getCitedBy(entry), citations);
- }
- }
-}
diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
index 9df5f2d2aaa..3d9c1b81129 100644
--- a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
+++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java
@@ -9,8 +9,6 @@
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.FilePreferences;
import org.jabref.logic.bibtex.FieldPreferences;
@@ -19,6 +17,7 @@
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.ImporterPreferences;
+import org.jabref.logic.importer.fetcher.CitationFetcher;
import org.jabref.logic.preferences.OwnerPreferences;
import org.jabref.logic.preferences.TimestampPreferences;
import org.jabref.logic.util.CurrentThreadTaskExecutor;
@@ -41,9 +40,7 @@
import static org.mockito.Mockito.when;
class CitationsRelationsTabViewModelTest {
- private ImportHandler importHandler;
private BibDatabaseContext bibDatabaseContext;
- private BibEntry testEntry;
@Mock
private GuiPreferences preferences;
diff --git a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java
new file mode 100644
index 00000000000..1e15af113f7
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java
@@ -0,0 +1,224 @@
+package org.jabref.logic.citation;
+
+import java.util.HashMap;
+import java.util.List;
+
+import org.jabref.logic.citation.repository.BibEntryRelationsRepositoryHelpersForTest;
+import org.jabref.logic.importer.fetcher.CitationFetcherHelpersForTest;
+import org.jabref.model.entry.BibEntry;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SearchCitationsRelationsServiceTest {
+
+ @Nested
+ class CitationsTests {
+ @Test
+ void serviceShouldSearchForCitations() {
+ // GIVEN
+ var cited = new BibEntry();
+ var citationsToReturn = List.of(new BibEntry());
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ e -> citationsToReturn, null, null, null, entry -> false, entry -> false
+ );
+ var searchService = new SearchCitationsRelationsService(null, repository);
+
+ // WHEN
+ List citations = searchService.searchCitations(cited);
+
+ // THEN
+ assertEquals(citationsToReturn, citations);
+ }
+
+ @Test
+ void serviceShouldCallTheFetcherForCitationsWhenRepositoryIsUpdatable() {
+ // GiVEN
+ var cited = new BibEntry();
+ var newCitations = new BibEntry();
+ var citationsToReturn = List.of(newCitations);
+ var citationsDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(
+ entry -> {
+ if (entry == cited) {
+ return citationsToReturn;
+ }
+ return List.of();
+ },
+ null
+ );
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ e -> citationsToReturn,
+ citationsDatabase::put,
+ List::of,
+ (e, r) -> { },
+ e -> true,
+ e -> false
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var citations = searchService.searchCitations(cited);
+
+ // THEN
+ assertTrue(citationsDatabase.containsKey(cited));
+ assertEquals(citationsToReturn, citationsDatabase.get(cited));
+ assertEquals(citationsToReturn, citations);
+ }
+
+ @Test
+ void serviceShouldFetchCitationsIfRepositoryIsEmpty() {
+ var cited = new BibEntry();
+ var newCitations = new BibEntry();
+ var citationsToReturn = List.of(newCitations);
+ var citationsDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(
+ entry -> {
+ if (entry == cited) {
+ return citationsToReturn;
+ }
+ return List.of();
+ },
+ null
+ );
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ citationsDatabase, null
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var citations = searchService.searchCitations(cited);
+
+ // THEN
+ assertTrue(citationsDatabase.containsKey(cited));
+ assertEquals(citationsToReturn, citationsDatabase.get(cited));
+ assertEquals(citationsToReturn, citations);
+ }
+
+ @Test
+ void insertingAnEmptyCitationsShouldBePossible() {
+ var cited = new BibEntry();
+ var citationsDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(
+ entry -> List.of(), null
+ );
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ citationsDatabase, null
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var citations = searchService.searchCitations(cited);
+
+ // THEN
+ assertTrue(citations.isEmpty());
+ assertTrue(citationsDatabase.containsKey(cited));
+ assertTrue(citationsDatabase.get(cited).isEmpty());
+ }
+ }
+
+ @Nested
+ class ReferencesTests {
+ @Test
+ void serviceShouldSearchForReferences() {
+ // GIVEN
+ var referencer = new BibEntry();
+ var referencesToReturn = List.of(new BibEntry());
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ null, null, e -> referencesToReturn, null, e -> false, e -> false
+ );
+ var searchService = new SearchCitationsRelationsService(null, repository);
+
+ // WHEN
+ List references = searchService.searchReferences(referencer);
+
+ // THEN
+ assertEquals(referencesToReturn, references);
+ }
+
+ @Test
+ void serviceShouldCallTheFetcherForReferencesWhenRepositoryIsUpdatable() {
+ // GIVEN
+ var referencer = new BibEntry();
+ var newReference = new BibEntry();
+ var referencesToReturn = List.of(newReference);
+ var referencesDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(null, entry -> {
+ if (entry == referencer) {
+ return referencesToReturn;
+ }
+ return List.of();
+ });
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ List::of,
+ (e, c) -> { },
+ e -> referencesToReturn,
+ referencesDatabase::put,
+ e -> false,
+ e -> true
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var references = searchService.searchReferences(referencer);
+
+ // THEN
+ assertTrue(referencesDatabase.containsKey(referencer));
+ assertEquals(referencesToReturn, referencesDatabase.get(referencer));
+ assertEquals(referencesToReturn, references);
+ }
+
+ @Test
+ void serviceShouldFetchReferencesIfRepositoryIsEmpty() {
+ var reference = new BibEntry();
+ var newCitations = new BibEntry();
+ var referencesToReturn = List.of(newCitations);
+ var referencesDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(
+ null,
+ entry -> {
+ if (entry == reference) {
+ return referencesToReturn;
+ }
+ return List.of();
+ }
+ );
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ null, referencesDatabase
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var references = searchService.searchReferences(reference);
+
+ // THEN
+ assertTrue(referencesDatabase.containsKey(reference));
+ assertEquals(referencesToReturn, referencesDatabase.get(reference));
+ assertEquals(referencesToReturn, references);
+ }
+
+ @Test
+ void insertingAnEmptyReferencesShouldBePossible() {
+ var referencer = new BibEntry();
+ var referenceDatabase = new HashMap>();
+ var fetcher = CitationFetcherHelpersForTest.Mocks.from(
+ null, entry -> List.of()
+ );
+ var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from(
+ null, referenceDatabase
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var citations = searchService.searchReferences(referencer);
+
+ // THEN
+ assertTrue(citations.isEmpty());
+ assertTrue(referenceDatabase.containsKey(referencer));
+ assertTrue(referenceDatabase.get(referencer).isEmpty());
+ }
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java
new file mode 100644
index 00000000000..d83156f0745
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java
@@ -0,0 +1,181 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.random.RandomGenerator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.jabref.model.citation.semanticscholar.PaperDetails;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.identifier.DOI;
+import org.jabref.model.entry.types.StandardEntryType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class BibEntryRelationDAOChainTest {
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(BibEntryRelationDAOChainTest::createBibEntry);
+ }
+
+ private static BibEntry createBibEntry(int i) {
+ return new BibEntry()
+ .withCitationKey(String.valueOf(i))
+ .withField(StandardField.DOI, "10.1234/5678" + i);
+ }
+
+ /**
+ * Create a fake list of relations for a bibEntry based on the {@link PaperDetails#toBibEntry()} logic
+ * that corresponds to this use case: we want to make sure that relations coming from SemanticScholar
+ * and mapped as BibEntry will be serializable by the MVStore.
+ *
+ * @param entry should not be null
+ * @return never empty
+ */
+ private static List createRelations(BibEntry entry) {
+ return entry
+ .getCitationKey()
+ .map(key -> RandomGenerator.StreamableGenerator
+ .of("L128X256MixRandom").ints(150)
+ .mapToObj(i -> new BibEntry()
+ .withField(StandardField.TITLE, "A title:" + i)
+ .withField(StandardField.YEAR, String.valueOf(2024))
+ .withField(StandardField.AUTHOR, "A list of authors:" + i)
+ .withType(StandardEntryType.Book)
+ .withField(StandardField.DOI, entry.getDOI().map(DOI::getDOI).orElse("") + ":" + i)
+ .withField(StandardField.URL, "www.jabref.org/" + i)
+ .withField(StandardField.ABSTRACT, "The Universe is expanding:" + i)
+ )
+ )
+ .orElseThrow()
+ .collect(Collectors.toList());
+ }
+
+ private static Stream createCacheAndBibEntry() {
+ return Stream
+ .of(LRUCacheBibEntryRelationsDAO.CITATIONS, LRUCacheBibEntryRelationsDAO.REFERENCES)
+ .flatMap(dao -> {
+ dao.clearEntries();
+ return createBibEntries().map(entry -> Arguments.of(dao, entry));
+ });
+ }
+
+ private static class DaoMock implements BibEntryRelationDAO {
+
+ Map> table = new HashMap<>();
+
+ @Override
+ public List getRelations(BibEntry entry) {
+ return this.table.getOrDefault(entry, List.of());
+ }
+
+ @Override
+ public void cacheOrMergeRelations(BibEntry entry, List relations) {
+ this.table.put(entry, relations);
+ }
+
+ @Override
+ public boolean containsKey(BibEntry entry) {
+ return this.table.containsKey(entry);
+ }
+
+ @Override
+ public boolean isUpdatable(BibEntry entry) {
+ return !this.containsKey(entry);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldReadFromFirstNode(BibEntryRelationDAO dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ dao.cacheOrMergeRelations(entry, relations);
+ var secondDao = new DaoMock();
+ var doaChain = BibEntryRelationDAOChain.of(dao, secondDao);
+
+ // WHEN
+ var relationsFromChain = doaChain.getRelations(entry);
+
+ // THEN
+ Assertions.assertEquals(relations, relationsFromChain);
+ Assertions.assertEquals(relations, dao.getRelations(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldReadFromSecondNode(BibEntryRelationDAO dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ dao.cacheOrMergeRelations(entry, relations);
+ var firstDao = new DaoMock();
+ var doaChain = BibEntryRelationDAOChain.of(firstDao, dao);
+
+ // WHEN
+ var relationsFromChain = doaChain.getRelations(entry);
+
+ // THEN
+ Assertions.assertEquals(relations, relationsFromChain);
+ Assertions.assertEquals(relations, dao.getRelations(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldReadFromSecondNodeAndRecopyToFirstNode(BibEntryRelationDAO dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ var firstDao = new DaoMock();
+ var doaChain = BibEntryRelationDAOChain.of(firstDao, dao);
+
+ // WHEN
+ doaChain.cacheOrMergeRelations(entry, relations);
+ var relationsFromChain = doaChain.getRelations(entry);
+
+ // THEN
+ Assertions.assertEquals(relations, relationsFromChain);
+ Assertions.assertEquals(relations, firstDao.getRelations(entry));
+ Assertions.assertEquals(relations, dao.getRelations(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldContainAKeyEvenIfItWasOnlyInsertedInLastNode(BibEntryRelationDAO secondDao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ var firstDao = new DaoMock();
+ var doaChain = BibEntryRelationDAOChain.of(firstDao, secondDao);
+
+ // WHEN
+ secondDao.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertFalse(firstDao.containsKey(entry));
+ Assertions.assertTrue(doaChain.containsKey(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldNotBeUpdatableBeforeInsertionAndNotAfterAnInsertion(BibEntryRelationDAO dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ var lastDao = new DaoMock();
+ var daoChain = BibEntryRelationDAOChain.of(dao, lastDao);
+ Assertions.assertTrue(daoChain.isUpdatable(entry));
+
+ // WHEN
+ daoChain.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertTrue(daoChain.containsKey(entry));
+ Assertions.assertFalse(daoChain.isUpdatable(entry));
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java
new file mode 100644
index 00000000000..d9a9b4bc920
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java
@@ -0,0 +1,109 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import org.jabref.model.entry.BibEntry;
+
+public class BibEntryRelationsRepositoryHelpersForTest {
+ public static class Mocks {
+ public static BibEntryRelationsRepository from(
+ Function> retrieveCitations,
+ BiConsumer> insertCitations,
+ Function> retrieveReferences,
+ BiConsumer> insertReferences,
+ Function isCitationsUpdatable,
+ Function isReferencesUpdatable
+ ) {
+ return new BibEntryRelationsRepository() {
+ @Override
+ public void insertCitations(BibEntry entry, List citations) {
+ insertCitations.accept(entry, citations);
+ }
+
+ @Override
+ public List readCitations(BibEntry entry) {
+ return retrieveCitations.apply(entry);
+ }
+
+ @Override
+ public boolean containsCitations(BibEntry entry) {
+ return true;
+ }
+
+ @Override
+ public boolean isCitationsUpdatable(BibEntry entry) {
+ return isCitationsUpdatable.apply(entry);
+ }
+
+ @Override
+ public void insertReferences(BibEntry entry, List citations) {
+ insertReferences.accept(entry, citations);
+ }
+
+ @Override
+ public List readReferences(BibEntry entry) {
+ return retrieveReferences.apply(entry);
+ }
+
+ @Override
+ public boolean containsReferences(BibEntry entry) {
+ return true;
+ }
+
+ @Override
+ public boolean isReferencesUpdatable(BibEntry entry) {
+ return isReferencesUpdatable.apply(entry);
+ }
+ };
+ }
+
+ public static BibEntryRelationsRepository from(
+ Map> citationsDB, Map> referencesDB
+ ) {
+ return new BibEntryRelationsRepository() {
+ @Override
+ public void insertCitations(BibEntry entry, List citations) {
+ citationsDB.put(entry, citations);
+ }
+
+ @Override
+ public List readCitations(BibEntry entry) {
+ return citationsDB.getOrDefault(entry, List.of());
+ }
+
+ @Override
+ public boolean containsCitations(BibEntry entry) {
+ return citationsDB.containsKey(entry);
+ }
+
+ @Override
+ public boolean isCitationsUpdatable(BibEntry entry) {
+ return true;
+ }
+
+ @Override
+ public void insertReferences(BibEntry entry, List citations) {
+ referencesDB.put(entry, citations);
+ }
+
+ @Override
+ public List readReferences(BibEntry entry) {
+ return referencesDB.getOrDefault(entry, List.of());
+ }
+
+ @Override
+ public boolean containsReferences(BibEntry entry) {
+ return referencesDB.containsKey(entry);
+ }
+
+ @Override
+ public boolean isReferencesUpdatable(BibEntry entry) {
+ return true;
+ }
+ };
+ }
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java
new file mode 100644
index 00000000000..d8d3bf8fbad
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java
@@ -0,0 +1,99 @@
+package org.jabref.logic.citation.repository;
+
+import java.nio.file.Files;
+import java.util.List;
+import java.util.random.RandomGenerator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+class ChainBibEntryRelationsRepositoryTest {
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(ChainBibEntryRelationsRepositoryTest::createBibEntry);
+ }
+
+ private static BibEntry createBibEntry(int i) {
+ return new BibEntry()
+ .withCitationKey(String.valueOf(i))
+ .withField(StandardField.DOI, "10.1234/5678" + i);
+ }
+
+ private static List createRelations(BibEntry entry) {
+ return entry
+ .getCitationKey()
+ .map(key -> RandomGenerator
+ .StreamableGenerator.of("L128X256MixRandom").ints(150)
+ .mapToObj(i -> new BibEntry()
+ .withCitationKey("%s relation %s".formatted(key, i))
+ .withField(StandardField.DOI, "10.2345/6789" + i)
+ )
+ )
+ .orElseThrow()
+ .toList();
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void repositoryShouldMergeCitationsWhenInserting(BibEntry bibEntry) throws Exception {
+ // GIVEN
+ var tempDir = Files.createTempDirectory("temp");
+ var mvStorePath = Files.createTempFile(tempDir, "cache", "");
+ var bibEntryRelationsRepository = new ChainBibEntryRelationsRepository(mvStorePath, mvStorePath);
+ assertFalse(bibEntryRelationsRepository.containsCitations(bibEntry));
+
+ // WHEN
+ var firstRelations = createRelations(bibEntry);
+ var secondRelations = createRelations(bibEntry);
+ bibEntryRelationsRepository.insertCitations(bibEntry, firstRelations);
+ bibEntryRelationsRepository.insertCitations(bibEntry, secondRelations);
+
+ // THEN
+ var uniqueRelations = Stream
+ .concat(firstRelations.stream(), secondRelations.stream())
+ .distinct()
+ .toList();
+ var relationFromCache = bibEntryRelationsRepository.readCitations(bibEntry);
+ assertFalse(uniqueRelations.isEmpty());
+ assertNotSame(uniqueRelations, relationFromCache);
+ assertEquals(uniqueRelations, relationFromCache);
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void repositoryShouldMergeReferencesWhenInserting(BibEntry bibEntry) throws Exception {
+ // GIVEN
+ var tempDir = Files.createTempDirectory("temp");
+ var mvStorePath = Files.createTempFile(tempDir, "cache", "");
+ var bibEntryRelationsRepository = new ChainBibEntryRelationsRepository(mvStorePath, mvStorePath);
+ assertFalse(bibEntryRelationsRepository.containsReferences(bibEntry));
+
+ // WHEN
+ var firstRelations = createRelations(bibEntry);
+ var secondRelations = createRelations(bibEntry);
+ bibEntryRelationsRepository.insertReferences(bibEntry, firstRelations);
+ bibEntryRelationsRepository.insertReferences(bibEntry, secondRelations);
+
+ // THEN
+ var uniqueRelations = Stream
+ .concat(firstRelations.stream(), secondRelations.stream())
+ .distinct()
+ .collect(Collectors.toList());
+ var relationFromCache = bibEntryRelationsRepository.readReferences(bibEntry);
+ assertFalse(uniqueRelations.isEmpty());
+ assertNotSame(uniqueRelations, relationFromCache);
+ assertEquals(uniqueRelations, relationFromCache);
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java
new file mode 100644
index 00000000000..409352a78b9
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java
@@ -0,0 +1,113 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+import java.util.random.RandomGenerator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.jabref.model.citation.semanticscholar.PaperDetails;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.identifier.DOI;
+import org.jabref.model.entry.types.StandardEntryType;
+
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class LRUCacheBibEntryRelationsDAOTest {
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(LRUCacheBibEntryRelationsDAOTest::createBibEntry);
+ }
+
+ private static BibEntry createBibEntry(int i) {
+ return new BibEntry()
+ .withCitationKey(String.valueOf(i))
+ .withField(StandardField.DOI, "10.1234/5678" + i);
+ }
+
+ /**
+ * Create a fake list of relations for a bibEntry based on the {@link PaperDetails#toBibEntry()} logic
+ * that corresponds to this use case: we want to make sure that relations coming from SemanticScholar
+ * and mapped as BibEntry will be serializable by the MVStore.
+ * @param entry should not be null
+ * @return never empty
+ */
+ private static List createRelations(BibEntry entry) {
+ return entry
+ .getCitationKey()
+ .map(key -> RandomGenerator.StreamableGenerator
+ .of("L128X256MixRandom").ints(150)
+ .mapToObj(i -> new BibEntry()
+ .withField(StandardField.TITLE, "A title:" + i)
+ .withField(StandardField.YEAR, String.valueOf(2024))
+ .withField(StandardField.AUTHOR, "A list of authors:" + i)
+ .withType(StandardEntryType.Book)
+ .withField(StandardField.DOI, entry.getDOI().map(DOI::getDOI).orElse("") + ":" + i)
+ .withField(StandardField.URL, "www.jabref.org/" + i)
+ .withField(StandardField.ABSTRACT, "The Universe is expanding:" + i)
+ )
+ )
+ .orElseThrow()
+ .collect(Collectors.toList());
+ }
+
+ private static Stream createCacheAndBibEntry() {
+ return Stream
+ .of(LRUCacheBibEntryRelationsDAO.CITATIONS, LRUCacheBibEntryRelationsDAO.REFERENCES)
+ .flatMap(dao -> createBibEntries().map(entry -> Arguments.of(dao, entry)));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void repositoryShouldMergeCitationsWhenInserting(LRUCacheBibEntryRelationsDAO dao, BibEntry entry) {
+ // GIVEN
+ dao.clearEntries();
+ assertFalse(dao.containsKey(entry));
+
+ // WHEN
+ var firstRelations = createRelations(entry);
+ var secondRelations = createRelations(entry);
+ dao.cacheOrMergeRelations(entry, firstRelations);
+ dao.cacheOrMergeRelations(entry, secondRelations);
+
+ // THEN
+ var uniqueRelations = Stream
+ .concat(firstRelations.stream(), secondRelations.stream())
+ .distinct()
+ .toList();
+ var relationFromCache = dao.getRelations(entry);
+ assertTrue(dao.containsKey(entry));
+ assertFalse(uniqueRelations.isEmpty());
+ assertNotSame(uniqueRelations, relationFromCache);
+ assertEquals(uniqueRelations, relationFromCache);
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void clearingCacheShouldWork(LRUCacheBibEntryRelationsDAO dao, BibEntry entry) {
+ // GIVEN
+ dao.clearEntries();
+ var relations = createRelations(entry);
+ assertFalse(dao.containsKey(entry));
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+ assertTrue(dao.containsKey(entry));
+ dao.clearEntries();
+
+ // THEN
+ assertFalse(dao.containsKey(entry));
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java
new file mode 100644
index 00000000000..1816ed10096
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java
@@ -0,0 +1,166 @@
+package org.jabref.logic.citation.repository;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.random.RandomGenerator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.jabref.model.citation.semanticscholar.PaperDetails;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.identifier.DOI;
+import org.jabref.model.entry.types.StandardEntryType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+class MVStoreBibEntryRelationsRepositoryDAOTest {
+
+ private final static String TEMPORARY_FOLDER_NAME = "bib_entry_relations_test_not_contains_store";
+ private final static String MAP_NAME = "test-relations";
+
+ @TempDir Path temporaryFolder;
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(MVStoreBibEntryRelationsRepositoryDAOTest::createBibEntry);
+ }
+
+ private static BibEntry createBibEntry(int i) {
+ return new BibEntry()
+ .withCitationKey(String.valueOf(i))
+ .withField(StandardField.DOI, "10.1234/5678" + i);
+ }
+
+ /**
+ * Create a fake list of relations for a bibEntry based on the {@link PaperDetails#toBibEntry()} logic
+ * that corresponds to this use case: we want to make sure that relations coming from SemanticScholar
+ * and mapped as BibEntry will be serializable by the MVStore.
+ *
+ * @param entry should not be null
+ * @return never empty
+ */
+ private static List createRelations(BibEntry entry) {
+ return entry
+ .getCitationKey()
+ .map(key -> RandomGenerator.StreamableGenerator
+ .of("L128X256MixRandom").ints(150)
+ .mapToObj(i -> new BibEntry()
+ .withField(StandardField.TITLE, "A title:" + i)
+ .withField(StandardField.YEAR, String.valueOf(2024))
+ .withField(StandardField.AUTHOR, "A list of authors:" + i)
+ .withType(StandardEntryType.Book)
+ .withField(StandardField.DOI, entry.getDOI().map(DOI::getDOI).orElse("") + ":" + i)
+ .withField(StandardField.URL, "www.jabref.org/" + i)
+ .withField(StandardField.ABSTRACT, "The Universe is expanding:" + i)
+ )
+ )
+ .orElseThrow()
+ .collect(Collectors.toList());
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void DAOShouldMergeRelationsWhenInserting(BibEntry bibEntry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME);
+ Assertions.assertFalse(dao.containsKey(bibEntry));
+ var firstRelations = createRelations(bibEntry);
+ var secondRelations = createRelations(bibEntry);
+
+ // WHEN
+ dao.cacheOrMergeRelations(bibEntry, firstRelations);
+ dao.cacheOrMergeRelations(bibEntry, secondRelations);
+ var relationFromCache = dao.getRelations(bibEntry);
+
+ // THEN
+ var uniqueRelations = Stream
+ .concat(firstRelations.stream(), secondRelations.stream())
+ .distinct()
+ .toList();
+ assertFalse(uniqueRelations.isEmpty());
+ assertNotSame(uniqueRelations, relationFromCache);
+ assertEquals(uniqueRelations, relationFromCache);
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void containsKeyShouldReturnFalseIfNothingWasInserted(BibEntry entry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME);
+
+ // THEN
+ Assertions.assertFalse(dao.containsKey(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void containsKeyShouldReturnTrueIfRelationsWereInserted(BibEntry entry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME);
+ var relations = createRelations(entry);
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertTrue(dao.containsKey(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void isUpdatableShouldReturnTrueBeforeInsertionsAndFalseAfterInsertions(BibEntry entry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME);
+ var relations = createRelations(entry);
+ Assertions.assertTrue(dao.isUpdatable(entry));
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertFalse(dao.isUpdatable(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void isUpdatableShouldReturnTrueAfterOneWeek(BibEntry entry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME);
+ var relations = createRelations(entry);
+ var clock = Clock.fixed(Instant.now(), ZoneId.of("UTC"));
+ Assertions.assertTrue(dao.isUpdatable(entry, clock));
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertFalse(dao.isUpdatable(entry, clock));
+ var clockOneWeekAfter = Clock.fixed(
+ LocalDateTime.now(ZoneId.of("UTC")).plusWeeks(1).toInstant(ZoneOffset.UTC),
+ ZoneId.of("UTC")
+ );
+ Assertions.assertTrue(dao.isUpdatable(entry, clockOneWeekAfter));
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java
new file mode 100644
index 00000000000..8c7d6ced91f
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java
@@ -0,0 +1,280 @@
+package org.jabref.logic.citation.repository;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.random.RandomGenerator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.jabref.model.citation.semanticscholar.PaperDetails;
+import org.jabref.model.entry.BibEntry;
+import org.jabref.model.entry.field.StandardField;
+import org.jabref.model.entry.identifier.DOI;
+import org.jabref.model.entry.types.StandardEntryType;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.h2.mvstore.DataUtils;
+import org.h2.mvstore.MVMap;
+import org.h2.mvstore.MVStore;
+import org.h2.mvstore.WriteBuffer;
+import org.h2.mvstore.type.BasicDataType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class MVStoreBibEntryRelationsRepositoryPrototypingTest {
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(MVStoreBibEntryRelationsRepositoryPrototypingTest::createBibEntry);
+ }
+
+ private static BibEntry createBibEntry(int i) {
+ return new BibEntry()
+ .withCitationKey(String.valueOf(i))
+ .withField(StandardField.DOI, "10.1234/5678" + i);
+ }
+
+ /**
+ * Create a fake list of relations for a bibEntry based on the {@link PaperDetails#toBibEntry()} logic
+ * that corresponds to this use case: we want to make sure that relations coming from SemanticScholar
+ * and mapped as BibEntry will be serializable by the MVStore.
+ * @param entry should not be null
+ * @return never empty
+ */
+ private static LinkedHashSet createRelations(BibEntry entry) {
+ return entry
+ .getCitationKey()
+ .map(key -> RandomGenerator.StreamableGenerator
+ .of("L128X256MixRandom").ints(150)
+ .mapToObj(i -> new BibEntry()
+ .withField(StandardField.TITLE, "A title:" + i)
+ .withField(StandardField.YEAR, String.valueOf(2024))
+ .withField(StandardField.AUTHOR, "A list of authors:" + i)
+ .withType(StandardEntryType.Book)
+ .withField(StandardField.DOI, entry.getDOI().map(DOI::getDOI).orElse("") + ":" + i)
+ .withField(StandardField.URL, "www.jabref.org/" + i)
+ .withField(StandardField.ABSTRACT, "The Universe is expanding:" + i)
+ )
+ )
+ .orElseThrow()
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ static class BibEntrySerializer extends BasicDataType {
+
+ private final static String FIELD_SEPARATOR = "--";
+
+ private static String toString(BibEntry entry) {
+ return String.join(
+ FIELD_SEPARATOR,
+ entry.getTitle().orElse("null"),
+ entry.getField(StandardField.YEAR).orElse("null"),
+ entry.getField(StandardField.AUTHOR).orElse("null"),
+ entry.getType().getDisplayName(),
+ entry.getDOI().map(DOI::getDOI).orElse("null"),
+ entry.getField(StandardField.URL).orElse("null"),
+ entry.getField(StandardField.ABSTRACT).orElse("null")
+ );
+ }
+
+ private static Optional extractFieldValue(String field) {
+ return Objects.equals(field, "null") || field == null
+ ? Optional.empty()
+ : Optional.of(field);
+ }
+
+ private static BibEntry fromString(String serializedString) {
+ var fields = serializedString.split(FIELD_SEPARATOR);
+ BibEntry entry = new BibEntry();
+ extractFieldValue(fields[0]).ifPresent(title -> entry.setField(StandardField.TITLE, title));
+ extractFieldValue(fields[1]).ifPresent(year -> entry.setField(StandardField.YEAR, year));
+ extractFieldValue(fields[2]).ifPresent(authors -> entry.setField(StandardField.AUTHOR, authors));
+ extractFieldValue(fields[3]).ifPresent(type -> entry.setType(StandardEntryType.valueOf(type)));
+ extractFieldValue(fields[4]).ifPresent(doi -> entry.setField(StandardField.DOI, doi));
+ extractFieldValue(fields[5]).ifPresent(url -> entry.setField(StandardField.URL, url));
+ extractFieldValue(fields[6])
+ .ifPresent(entryAbstract -> entry.setField(StandardField.ABSTRACT, entryAbstract));
+ return entry;
+ }
+
+ @Override
+ public int getMemory(BibEntry obj) {
+ return toString(obj).getBytes(StandardCharsets.UTF_8).length;
+ }
+
+ @Override
+ public void write(WriteBuffer buff, BibEntry bibEntry) {
+ var asBytes = toString(bibEntry).getBytes(StandardCharsets.UTF_8);
+ buff.putInt(asBytes.length);
+ buff.put(asBytes);
+ }
+
+ @Override
+ public BibEntry read(ByteBuffer buff) {
+ int serializedEntrySize = buff.getInt();
+ var serializedEntry = DataUtils.readString(buff, serializedEntrySize);
+ return fromString(serializedEntry);
+ }
+
+ @Override
+ public int compare(BibEntry a, BibEntry b) {
+ if (a == null || b == null) {
+ throw new NullPointerException();
+ }
+ return toString(a).compareTo(toString(b));
+ }
+
+ @Override
+ public BibEntry[] createStorage(int size) {
+ return new BibEntry[size];
+ }
+ }
+
+ static class BibEntryHashSetSerializer extends BasicDataType> {
+
+ private final BasicDataType bibEntryDataType = new BibEntrySerializer();
+
+ /**
+ * Memory size is the sum of all aggregated bibEntries memory size plus 4 bytes.
+ * Those 4 bytes are used to store the length of the collection itself.
+ * @param bibEntries should not be null
+ * @return total size in memory of the serialized collection of bib entries
+ */
+ @Override
+ public int getMemory(LinkedHashSet bibEntries) {
+ return bibEntries
+ .stream()
+ .map(this.bibEntryDataType::getMemory)
+ .reduce(0, Integer::sum) + 4;
+ }
+
+ @Override
+ public void write(WriteBuffer buff, LinkedHashSet bibEntries) {
+ buff.putInt(bibEntries.size());
+ bibEntries.forEach(entry -> this.bibEntryDataType.write(buff, entry));
+ }
+
+ @Override
+ public LinkedHashSet read(ByteBuffer buff) {
+ return IntStream.range(0, buff.getInt())
+ .mapToObj(it -> this.bibEntryDataType.read(buff))
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ @Override
+ public LinkedHashSet[] createStorage(int size) {
+ return new LinkedHashSet[size];
+ }
+ }
+
+ @Test
+ void itShouldBePossibleToStoreABibEntryList(@TempDir Path temporaryFolder) throws IOException {
+ var file = Files.createFile(temporaryFolder.resolve("test_string_store"));
+ try (var store = new MVStore.Builder().fileName(file.toAbsolutePath().toString()).open()) {
+ // GIVEN
+ MVMap> citations = store.openMap("citations");
+
+ // WHEN
+ citations.put("Hello", List.of("The", "World"));
+ store.commit();
+ var fromStore = citations.get("Hello");
+
+ // THEN
+ Assertions.assertTrue(Files.exists(file));
+ Assertions.assertEquals("Hello The World", "Hello " + String.join(" ", fromStore));
+ }
+ }
+
+ /**
+ * Fake in memory sequential save and load
+ */
+ @Test
+ void IWouldLikeToSaveAndLoadCitationsForABibEntryFromAMap(@TempDir Path temporaryFolder) throws IOException {
+ var file = Files.createFile(temporaryFolder.resolve("bib_entry_citations_test_store"));
+ try (var store = new MVStore.Builder().fileName(file.toAbsolutePath().toString()).open()) {
+ // GIVEN
+ Map> citationsToBeStored = createBibEntries()
+ .map(e -> Pair.of(e.getDOI().orElseThrow().getDOI(), createRelations(e)))
+ .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
+ Assertions.assertFalse(citationsToBeStored.isEmpty());
+ var mapConfiguration = new MVMap.Builder>()
+ .valueType(new BibEntryHashSetSerializer());
+
+ /**
+ var mapConfiguration = new MVMap.Builder>()
+ .valueType(new BibEntryHashSetSerializer());
+ MVMap> citationsMap = store.openMap("citations", mapConfiguration);
+ **/
+ MVMap> citationsMap = store.openMap("citations", mapConfiguration);
+
+ // WHEN
+ citationsToBeStored.forEach((entry, citations) -> citationsMap.put(entry, new LinkedHashSet<>(citations)));
+
+ // THEN
+ citationsToBeStored.forEach((entry, citations) -> {
+ Assertions.assertTrue(citationsMap.containsKey(entry));
+ Assertions.assertEquals(citations, citationsMap.get(entry));
+ });
+ }
+ }
+
+ /**
+ * Fake persisted sequential save and load operations.
+ */
+ @Test
+ void IWouldLikeToSaveAndLoadCitationsForABibEntryFromAStore(@TempDir Path temporaryFolder) throws IOException {
+ var file = Files.createFile(temporaryFolder.resolve("bib_entry_citations_test_store"));
+
+ // GIVEN
+ Map> citationsToBeStored = createBibEntries()
+ .map(e -> Pair.of(e.getDOI().orElseThrow().getDOI(), createRelations(e)))
+ .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
+ Assertions.assertFalse(citationsToBeStored.isEmpty());
+
+ var mapConfiguration = new MVMap.Builder>()
+ .valueType(new BibEntryHashSetSerializer());
+
+ Map> citationsFromStore = null;
+
+ // WHEN
+ // STORING AND CLOSING
+ try (var store = new MVStore.Builder().fileName(file.toAbsolutePath().toString()).open()) {
+ MVMap> citationsMap = store.openMap("citations", mapConfiguration);
+ citationsToBeStored.forEach((entry, citations) -> citationsMap.put(entry, new LinkedHashSet<>(citations)));
+ store.commit();
+ }
+
+ // READING AND CLOSING
+ try (var store = new MVStore.Builder().fileName(file.toAbsolutePath().toString()).open()) {
+ MVMap> citationsMap = store.openMap("citations", mapConfiguration);
+ citationsFromStore = Map.copyOf(citationsMap);
+ }
+
+ // THEN
+ Assertions.assertNotNull(citationsFromStore);
+ Assertions.assertFalse(citationsFromStore.isEmpty());
+ var entriesToBeStored = citationsToBeStored.entrySet();
+ for (var entry : entriesToBeStored) {
+ Assertions.assertTrue(citationsFromStore.containsKey(entry.getKey()));
+ var citations = citationsFromStore.get(entry.getKey());
+ Assertions.assertEquals(entry.getValue(), citations);
+ }
+ }
+
+ @Test
+ void test() {
+ var s = Stream.of(null, "test", null).collect(Collectors.joining());
+ System.out.println(s);
+ }
+}
diff --git a/src/test/java/org/jabref/logic/importer/fetcher/CitationFetcherHelpersForTest.java b/src/test/java/org/jabref/logic/importer/fetcher/CitationFetcherHelpersForTest.java
new file mode 100644
index 00000000000..8a33679359e
--- /dev/null
+++ b/src/test/java/org/jabref/logic/importer/fetcher/CitationFetcherHelpersForTest.java
@@ -0,0 +1,32 @@
+package org.jabref.logic.importer.fetcher;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.jabref.model.entry.BibEntry;
+
+public class CitationFetcherHelpersForTest {
+ public static class Mocks {
+ public static CitationFetcher from(
+ Function> retrieveCitedBy,
+ Function> retrieveCiting
+ ) {
+ return new CitationFetcher() {
+ @Override
+ public List searchCitedBy(BibEntry entry) {
+ return retrieveCitedBy.apply(entry);
+ }
+
+ @Override
+ public List searchCiting(BibEntry entry) {
+ return retrieveCiting.apply(entry);
+ }
+
+ @Override
+ public String getName() {
+ return "Test citation fetcher";
+ }
+ };
+ }
+ }
+}