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..1ead7439e22 100644
--- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
+++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java
@@ -1,8 +1,12 @@
package org.jabref.gui.entryeditor.citationrelationtab;
+import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
@@ -36,8 +40,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 +47,12 @@
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.bibtex.FieldWriter;
+import org.jabref.logic.citation.repository.ChainBibEntryRelationsRepository;
+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.importer.fetcher.SemanticScholarCitationFetcher;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.os.OS;
import org.jabref.logic.util.BackgroundTask;
@@ -85,7 +91,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;
@@ -109,9 +115,29 @@ 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);
+
+ try {
+ var jabRefPath = Paths.get("/home/sacha/Documents/projects/JabRef");
+ var citationsPath = Path.of(jabRefPath.toAbsolutePath() + File.separator + "citations");
+ var relationsPath = Path.of(jabRefPath.toAbsolutePath() + File.separator + "references");
+ var bibEntryRelationsRepository = new ChainBibEntryRelationsRepository(citationsPath, relationsPath);
+ this.searchCitationsRelationsService = new SearchCitationsRelationsService(
+ new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), bibEntryRelationsRepository
+ );
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+
+ citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(
+ databaseContext,
+ preferences,
+ undoManager,
+ stateManager,
+ dialogService,
+ fileUpdateMonitor,
+ taskExecutor
+ );
}
/**
@@ -399,47 +425,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, 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())));
-
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, 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 listView,
Button abortButton, Button refreshButton,
CitationFetcher.SearchType searchType, Button importButton,
@@ -456,7 +496,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..11d041abb7f
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java
@@ -0,0 +1,55 @@
+package org.jabref.logic.citation;
+
+import java.util.List;
+
+import org.jabref.logic.citation.repository.BibEntryRelationsRepository;
+import org.jabref.logic.importer.FetcherException;
+import org.jabref.logic.importer.fetcher.CitationFetcher;
+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 List searchReferences(BibEntry referencer, boolean forceUpdate) {
+ if (forceUpdate || !this.relationsRepository.containsReferences(referencer)) {
+ try {
+ var references = this.citationFetcher.searchCiting(referencer);
+ if (!references.isEmpty()) {
+ 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 forceUpdate) {
+ if (forceUpdate || !this.relationsRepository.containsCitations(cited)) {
+ try {
+ var citations = this.citationFetcher.searchCitedBy(cited);
+ if (!citations.isEmpty()) {
+ 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..e56b08f4d3f
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java
@@ -0,0 +1,14 @@
+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);
+}
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..84b4c73a1d7
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java
@@ -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 citations);
+
+ List readCitations(BibEntry entry);
+
+ boolean containsCitations(BibEntry entry);
+
+ void insertReferences(BibEntry entry, List citations);
+
+ List readReferences(BibEntry entry);
+
+ boolean containsReferences(BibEntry entry);
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java
new file mode 100644
index 00000000000..3b802f7cada
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java
@@ -0,0 +1,53 @@
+package org.jabref.logic.citation.repository;
+
+import java.util.List;
+
+import org.jabref.model.entry.BibEntry;
+
+public class ChainBibEntryRelationDAO implements BibEntryRelationDAO {
+
+ private static final BibEntryRelationDAO EMPTY = new ChainBibEntryRelationDAO(null, null);
+
+ private final BibEntryRelationDAO current;
+ private final BibEntryRelationDAO next;
+
+ ChainBibEntryRelationDAO(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));
+ }
+
+ public static BibEntryRelationDAO of(BibEntryRelationDAO... dao) {
+ return List.of(dao)
+ .reversed()
+ .stream()
+ .reduce(EMPTY, (acc, current) -> new ChainBibEntryRelationDAO(current, acc));
+ }
+}
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..289b08edb8f
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java
@@ -0,0 +1,56 @@
+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 final BibEntryRelationDAO citationsDao;
+ private final BibEntryRelationDAO referencesDao;
+
+ public ChainBibEntryRelationsRepository(Path citationsStore, Path relationsStore) {
+ this.citationsDao = ChainBibEntryRelationDAO.of(
+ LRUCacheBibEntryRelationsDAO.CITATIONS, new MVStoreBibEntryRelationDAO(citationsStore, "citations")
+ );
+ this.referencesDao = ChainBibEntryRelationDAO.of(
+ LRUCacheBibEntryRelationsDAO.REFERENCES, new MVStoreBibEntryRelationDAO(relationsStore, "relations")
+ );
+ }
+
+ @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 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);
+ }
+}
diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java
new file mode 100644
index 00000000000..f87455fc033
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java
@@ -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> 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 entry
+ .getDOI()
+ .stream()
+ .flatMap(doi -> CITATIONS_MAP.getOrDefault(doi, Set.of()).stream())
+ .toList();
+ }
+
+ public List getReferences(BibEntry entry) {
+ return entry
+ .getDOI()
+ .stream()
+ .flatMap(doi -> REFERENCES_MAP.getOrDefault(doi, Set.of()).stream())
+ .toList();
+ }
+
+ public void cacheOrMergeCitations(BibEntry entry, List 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 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);
+ }
+}
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..017b3b93cb2
--- /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 = 100;
+ }
+
+ 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..fe0ee41be64
--- /dev/null
+++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java
@@ -0,0 +1,193 @@
+package org.jabref.logic.citation.repository;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+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 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.h2.mvstore.MVMap;
+import org.h2.mvstore.MVStore;
+import org.h2.mvstore.WriteBuffer;
+import org.h2.mvstore.type.BasicDataType;
+
+public class MVStoreBibEntryRelationDAO implements BibEntryRelationDAO {
+
+ private final String mapName;
+ private final MVStore.Builder storeConfiguration;
+ private final MVMap.Builder> mapConfiguration =
+ new MVMap.Builder>().valueType(new BibEntryHashSetSerializer());
+
+ MVStoreBibEntryRelationDAO(Path path, String mapName) {
+ this.mapName = mapName;
+ 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());
+ }
+
+ @Override
+ synchronized public void cacheOrMergeRelations(BibEntry entry, List relations) {
+ entry.getDOI().ifPresent(doi -> {
+ try (var store = this.storeConfiguration.open()) {
+ MVMap> relationsMap = store.openMap(mapName, mapConfiguration);
+ var relationsAlreadyStored = relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>());
+ relationsAlreadyStored.addAll(relations);
+ relationsMap.put(doi.getDOI(), relationsAlreadyStored);
+ store.commit();
+ }
+ });
+ }
+
+ @Override
+ 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);
+ }
+
+ 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/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..25097ed4b87
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java
@@ -0,0 +1,179 @@
+package org.jabref.logic.citation.service;
+
+import java.util.HashMap;
+import java.util.List;
+
+import org.jabref.logic.citation.SearchCitationsRelationsService;
+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
+ );
+ var searchService = new SearchCitationsRelationsService(null, repository);
+
+ // WHEN
+ List citations = searchService.searchCitations(cited, false);
+
+ // THEN
+ assertEquals(citationsToReturn, citations);
+ }
+
+ @Test
+ void serviceShouldForceCitationsUpdate() {
+ // 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) -> { }
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var citations = searchService.searchCitations(cited, true);
+
+ // 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, false);
+
+ // THEN
+ assertTrue(citationsDatabase.containsKey(cited));
+ assertEquals(citationsToReturn, citationsDatabase.get(cited));
+ assertEquals(citationsToReturn, citations);
+ }
+ }
+
+ @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
+ );
+ var searchService = new SearchCitationsRelationsService(null, repository);
+
+ // WHEN
+ List references = searchService.searchReferences(referencer, false);
+
+ // THEN
+ assertEquals(referencesToReturn, references);
+ }
+
+ @Test
+ void serviceShouldCallTheFetcherForReferencesIWhenForceUpdateIsTrue() {
+ // 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
+ );
+ var searchService = new SearchCitationsRelationsService(fetcher, repository);
+
+ // WHEN
+ var references = searchService.searchReferences(referencer, true);
+
+ // 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, false);
+
+ // THEN
+ assertTrue(referencesDatabase.containsKey(reference));
+ assertEquals(referencesToReturn, referencesDatabase.get(reference));
+ assertEquals(referencesToReturn, references);
+ }
+ }
+}
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..f2305b38b33
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java
@@ -0,0 +1,87 @@
+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
+ ) {
+ 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 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;
+ }
+ };
+ }
+
+ 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 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);
+ }
+ };
+ }
+ }
+}
diff --git a/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java
new file mode 100644
index 00000000000..6507c9b1d80
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java
@@ -0,0 +1,159 @@
+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 ChainBibEntryRelationDAOTest {
+
+ private static Stream createBibEntries() {
+ return IntStream
+ .range(0, 150)
+ .mapToObj(ChainBibEntryRelationDAOTest::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 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);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("createCacheAndBibEntry")
+ void theChainShouldReadFromFirstNode(BibEntryRelationDAO dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ dao.cacheOrMergeRelations(entry, relations);
+ var secondDao = new DaoMock();
+ var doaChain = ChainBibEntryRelationDAO.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 = ChainBibEntryRelationDAO.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 = ChainBibEntryRelationDAO.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 dao, BibEntry entry) {
+ // GIVEN
+ var relations = createRelations(entry);
+ var firstDao = new DaoMock();
+ var doaChain = ChainBibEntryRelationDAO.of(firstDao, dao);
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+ Assertions.assertFalse(firstDao.containsKey(entry));
+ boolean doesChainContainsTheKey = doaChain.containsKey(entry);
+
+ // THEN
+ Assertions.assertTrue(doesChainContainsTheKey);
+ }
+}
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..ad6a175cfaf
--- /dev/null
+++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java
@@ -0,0 +1,119 @@
+package org.jabref.logic.citation.repository;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+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 {
+
+ @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("bib_entry_relations_test_store"));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), "test-relations");
+ 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("bib_entry_relations_test_not_contains_store"));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), "test-relations");
+
+ // THEN
+ Assertions.assertFalse(dao.containsKey(entry));
+ }
+
+ @ParameterizedTest
+ @MethodSource("createBibEntries")
+ void containsKeyShouldReturnTrueIfRelationsWereInserted(BibEntry entry) throws IOException {
+ // GIVEN
+ var file = Files.createFile(temporaryFolder.resolve("bib_entry_relations_test_contains_store"));
+ var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), "test-relations");
+ var relations = createRelations(entry);
+
+ // WHEN
+ dao.cacheOrMergeRelations(entry, relations);
+
+ // THEN
+ Assertions.assertTrue(dao.containsKey(entry));
+ }
+}
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";
+ }
+ };
+ }
+ }
+}