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"; + } + }; + } + } +}