From 3785cb16c88e49808cbd41a58a45c178c804bed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Thu, 19 Sep 2024 23:07:33 +0200 Subject: [PATCH 01/10] Refactor Citations Relations Tab (#11189) * Move repository, cache, and fetcher to logic package * Move citations model to model/citations/semanticscholar package --- .../CitationRelationsTab.java | 14 +- .../CitationsRelationsTabViewModel.java | 2 +- .../repository}/BibEntryRelationsCache.java | 6 +- .../BibEntryRelationsRepository.java | 12 +- .../importer/fetcher}/CitationFetcher.java | 2 +- .../SemanticScholarCitationFetcher.java} | 9 +- .../semanticscholar/AuthorResponse.java | 2 +- .../semanticscholar/CitationDataItem.java | 2 +- .../semanticscholar/CitationsResponse.java | 2 +- .../semanticscholar/PaperDetails.java | 2 +- .../semanticscholar/ReferenceDataItem.java | 2 +- .../semanticscholar/ReferencesResponse.java | 2 +- .../BibEntryRelationsRepositoryTest.java | 61 --------- .../CitationsRelationsTabViewModelTest.java | 4 +- .../BibEntryRelationsRepositoryTest.java | 128 ++++++++++++++++++ 15 files changed, 159 insertions(+), 91 deletions(-) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => logic/citation/repository}/BibEntryRelationsCache.java (96%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => logic/citation/repository}/BibEntryRelationsRepository.java (82%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab/semanticscholar => logic/importer/fetcher}/CitationFetcher.java (94%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab/semanticscholar/SemanticScholarFetcher.java => logic/importer/fetcher/SemanticScholarCitationFetcher.java} (90%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/AuthorResponse.java (84%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/CitationDataItem.java (79%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/CitationsResponse.java (89%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/PaperDetails.java (98%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/ReferenceDataItem.java (70%) rename src/main/java/org/jabref/{gui/entryeditor/citationrelationtab => model/citation}/semanticscholar/ReferencesResponse.java (89%) delete mode 100644 src/test/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepositoryTest.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java 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 f6f8e9ca111..048d058462d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -30,8 +30,10 @@ import org.jabref.gui.StateManager; 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.logic.citation.repository.BibEntryRelationsCache; +import org.jabref.logic.citation.repository.BibEntryRelationsRepository; +import org.jabref.logic.importer.fetcher.CitationFetcher; +import org.jabref.logic.importer.fetcher.SemanticScholarCitationFetcher; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.util.NoSelectionModel; @@ -92,8 +94,10 @@ public CitationRelationsTab(DialogService dialogService, setTooltip(new Tooltip(Localization.lang("Show articles related by citation"))); this.duplicateCheck = new DuplicateCheck(new BibEntryTypesManager()); - this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()), - new BibEntryRelationsCache()); + this.bibEntryRelationsRepository = new BibEntryRelationsRepository( + new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), + new BibEntryRelationsCache() + ); citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(databaseContext, preferences, undoManager, stateManager, dialogService, fileUpdateMonitor, taskExecutor); } @@ -394,7 +398,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..0d0d5646ff1 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java @@ -8,7 +8,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; -import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.logic.importer.fetcher.CitationFetcher; import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.logic.citationkeypattern.CitationKeyGenerator; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java similarity index 96% rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java rename to src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java index 55888aa660f..fbccdef495f 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsCache.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java @@ -1,14 +1,12 @@ -package org.jabref.gui.entryeditor.citationrelationtab; +package org.jabref.logic.citation.repository; import java.util.Collections; import java.util.List; import java.util.Map; - +import org.eclipse.jgit.util.LRUMap; 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); diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java similarity index 82% rename from src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java rename to src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java index f7f29052da2..8c4ff85b36a 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/BibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java @@ -1,8 +1,8 @@ -package org.jabref.gui.entryeditor.citationrelationtab; +package org.jabref.logic.citation.repository; import java.util.List; -import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher; +import org.jabref.logic.importer.fetcher.CitationFetcher; import org.jabref.logic.importer.FetcherException; import org.jabref.model.entry.BibEntry; @@ -12,10 +12,10 @@ public class BibEntryRelationsRepository { private static final Logger LOGGER = LoggerFactory.getLogger(BibEntryRelationsRepository.class); - private final SemanticScholarFetcher fetcher; + private final CitationFetcher fetcher; private final BibEntryRelationsCache cache; - public BibEntryRelationsRepository(SemanticScholarFetcher fetcher, BibEntryRelationsCache cache) { + public BibEntryRelationsRepository(CitationFetcher fetcher, BibEntryRelationsCache cache) { this.fetcher = fetcher; this.cache = cache; } @@ -52,11 +52,11 @@ public void forceRefreshCitations(BibEntry entry) { } } - public boolean needToRefreshCitations(BibEntry entry) { + private boolean needToRefreshCitations(BibEntry entry) { return !cache.citationsCached(entry); } - public boolean needToRefreshReferences(BibEntry entry) { + private boolean needToRefreshReferences(BibEntry entry) { return !cache.referencesCached(entry); } 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..51a4762432d 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.entry.BibEntry; import com.google.gson.Gson; +import org.jabref.model.citation.semanticscholar.CitationsResponse; +import org.jabref.model.citation.semanticscholar.ReferencesResponse; -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/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/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..48e074fd00c 100644 --- a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java @@ -9,7 +9,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; -import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.logic.importer.fetcher.CitationFetcher; import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.logic.FilePreferences; @@ -41,9 +41,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/repository/BibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java new file mode 100644 index 00000000000..f17f6b8a99d --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java @@ -0,0 +1,128 @@ +package org.jabref.logic.citation.repository; + +import java.util.HashSet; +import java.util.List; + +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.fetcher.CitationFetcher; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class BibEntryRelationsRepositoryTest { + + private static Stream createBibEntries() { + return IntStream + .range(0, 150) + .mapToObj(BibEntryRelationsRepositoryTest::createBibEntry); + } + + private static List getCitedBy(BibEntry entry) { + return List.of(BibEntryRelationsRepositoryTest.createCitingBibEntry(entry)); + } + + private static BibEntry createBibEntry(int i) { + return new BibEntry() + .withCitationKey("entry" + i) + .withField(StandardField.DOI, "10.1234/5678" + i); + } + + private static BibEntry createCitingBibEntry(Integer i) { + return new BibEntry() + .withCitationKey("citing_entry" + i) + .withField(StandardField.DOI, "10.2345/6789" + i); + } + + private static BibEntry createCitingBibEntry(BibEntry citedEntry) { + return createCitingBibEntry( + Integer.valueOf(citedEntry.getCitationKey().orElseThrow().substring(5)) + ); + } + + /** + * Simple mock to avoid using Mockito (reduce overall complexity) + */ + private record CitationFetcherMock( + Function> searchCiteByDelegate, + Function> searchCitingDelegate, + String name + ) implements CitationFetcher { + + @Override + public List searchCitedBy(BibEntry entry) throws FetcherException { + return this.searchCiteByDelegate.apply(entry); + } + + @Override + public List searchCiting(BibEntry entry) throws FetcherException { + return this.searchCitingDelegate.apply(entry); + } + + @Override + public String getName() { + return this.name; + } + } + + @ParameterizedTest + @MethodSource("createBibEntries") + @DisplayName( + "Given a new bib entry when reading citations for it should call the fetcher" + ) + void givenANewEntryWhenReadingCitationsForItShouldCallTheFetcher(BibEntry bibEntry) { + // GIVEN + var entryCaptor = new HashSet(); + var citationFetcherMock = new CitationFetcherMock( + entry -> { + entryCaptor.add(entry); + return BibEntryRelationsRepositoryTest.getCitedBy(entry); + }, + null, + null + ); + var bibEntryRelationsCache = new BibEntryRelationsCache(); + var bibEntryRelationsRepository = new BibEntryRelationsRepository( + citationFetcherMock, bibEntryRelationsCache + ); + + // WHEN + var citations = bibEntryRelationsRepository.getCitations(bibEntry); + + // THEN + Assertions.assertFalse(citations.isEmpty()); + Assertions.assertTrue(entryCaptor.contains(bibEntry)); + } + + @Test + @DisplayName( + "Given an empty cache for a valid entry when reading the citations should populate cache" + ) + void givenAnEmptyCacheAndAValidBibEntryWhenReadingCitationsShouldPopulateTheCache() { + // GIVEN + var citationFetcherMock = new CitationFetcherMock( + BibEntryRelationsRepositoryTest::getCitedBy, null, null + ); + var bibEntryRelationsCache = new BibEntryRelationsCache(); + var bibEntryRelationsRepository = new BibEntryRelationsRepository( + citationFetcherMock, bibEntryRelationsCache + ); + var bibEntry = BibEntryRelationsRepositoryTest.createBibEntry(1); + + // WHEN + Assertions.assertEquals(List.of(), bibEntryRelationsCache.getCitations(bibEntry)); + var citations = bibEntryRelationsRepository.getCitations(bibEntry); + var fromCacheCitations = bibEntryRelationsCache.getCitations(bibEntry); + + // THEN + Assertions.assertFalse(fromCacheCitations.isEmpty()); + Assertions.assertEquals(citations, fromCacheCitations); + } +} From 8048da804d5b7905f5ae235128fec04ea3af23ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Tue, 24 Sep 2024 23:37:01 +0200 Subject: [PATCH 02/10] Refactor Citations Relations Tab (#11189) * Introduce service layer * Rename LRU cache implementation * Add tests helpers for repository --- .../BibEntryRelationsRepository.java | 87 +++++------------- ...he.java => LRUBibEntryRelationsCache.java} | 2 +- .../LRUBibEntryRelationsRepository.java | 78 ++++++++++++++++ .../SearchCitationsRelationsService.java | 36 ++++++++ .../BibEntryRelationsRepositoryTest.java | 12 +-- ...ibEntryRelationsRepositoryTestHelpers.java | 39 ++++++++ .../SearchCitationsRelationsServiceTest.java | 89 +++++++++++++++++++ 7 files changed, 269 insertions(+), 74 deletions(-) rename src/main/java/org/jabref/logic/citation/repository/{BibEntryRelationsCache.java => LRUBibEntryRelationsCache.java} (97%) create mode 100644 src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java create mode 100644 src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java create mode 100644 src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java index 8c4ff85b36a..48f38e2a38d 100644 --- a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java @@ -1,73 +1,26 @@ package org.jabref.logic.citation.repository; import java.util.List; - -import org.jabref.logic.importer.fetcher.CitationFetcher; -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 CitationFetcher fetcher; - private final BibEntryRelationsCache cache; - - public BibEntryRelationsRepository(CitationFetcher 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); - } - } - - private boolean needToRefreshCitations(BibEntry entry) { - return !cache.citationsCached(entry); - } - - private 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); - } +public interface BibEntryRelationsRepository { + List readCitations(BibEntry entry); + + List readReferences(BibEntry entry); + + /** + * Fetch citations for a bib entry and update local database. + * @param entry should not be null + * @deprecated fetching citations should be done by the service layer (calling code) + */ + @Deprecated + void forceRefreshCitations(BibEntry entry); + + /** + * Fetch references made by a bib entry and update local database. + * @param entry should not be null + * @deprecated fetching references should be done by the service layer (calling code) + */ + @Deprecated + void forceRefreshReferences(BibEntry entry); } diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java similarity index 97% rename from src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java rename to src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java index fbccdef495f..8e6d491ce75 100644 --- a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsCache.java +++ b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java @@ -7,7 +7,7 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.identifier.DOI; -public class BibEntryRelationsCache { +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); diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java new file mode 100644 index 00000000000..23ab083ee4b --- /dev/null +++ b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java @@ -0,0 +1,78 @@ +package org.jabref.logic.citation.repository; + +import java.util.List; + +import org.jabref.logic.importer.fetcher.CitationFetcher; +import org.jabref.logic.importer.FetcherException; +import org.jabref.model.entry.BibEntry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LRUBibEntryRelationsRepository implements BibEntryRelationsRepository { + private static final Logger LOGGER = LoggerFactory + .getLogger(LRUBibEntryRelationsRepository.class); + + private final CitationFetcher fetcher; + private final LRUBibEntryRelationsCache cache; + + public LRUBibEntryRelationsRepository(CitationFetcher fetcher, LRUBibEntryRelationsCache cache) { + this.fetcher = fetcher; + this.cache = cache; + } + + @Override + public List readCitations(BibEntry entry) { + if (needToRefreshCitations(entry)) { + forceRefreshCitations(entry); + } + + return cache.getCitations(entry); + } + + @Override + public List readReferences(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); + } + + @Override + 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); + } + } + + private boolean needToRefreshCitations(BibEntry entry) { + return !cache.citationsCached(entry); + } + + private boolean needToRefreshReferences(BibEntry entry) { + return !cache.referencesCached(entry); + } + + @Override + 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/logic/citation/service/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java new file mode 100644 index 00000000000..210821d708d --- /dev/null +++ b/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java @@ -0,0 +1,36 @@ +package org.jabref.logic.citation.service; + +import java.util.List; +import org.jabref.logic.citation.repository.BibEntryRelationsRepository; +import org.jabref.model.entry.BibEntry; + +public class SearchCitationsRelationsService { + + BibEntryRelationsRepository relationsRepository; + + public SearchCitationsRelationsService(BibEntryRelationsRepository repository) { + this.relationsRepository = repository; + } + + public List searchReferences(BibEntry referencer) { + return this.relationsRepository.readReferences(referencer); + } + + public List searchReferences(BibEntry referencer, boolean forceUpdate) { + if (forceUpdate) { + this.relationsRepository.forceRefreshReferences(referencer); + } + return this.searchReferences(referencer); + } + + public List searchCitations(BibEntry cited) { + return this.relationsRepository.readCitations(cited); + } + + public List searchCitations(BibEntry cited, boolean forceUpdate) { + if (forceUpdate) { + this.relationsRepository.forceRefreshCitations(cited); + } + return this.searchCitations(cited); + } +} diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java index f17f6b8a99d..0b436ac9187 100644 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java @@ -88,13 +88,13 @@ void givenANewEntryWhenReadingCitationsForItShouldCallTheFetcher(BibEntry bibEnt null, null ); - var bibEntryRelationsCache = new BibEntryRelationsCache(); - var bibEntryRelationsRepository = new BibEntryRelationsRepository( + var bibEntryRelationsCache = new LRUBibEntryRelationsCache(); + var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( citationFetcherMock, bibEntryRelationsCache ); // WHEN - var citations = bibEntryRelationsRepository.getCitations(bibEntry); + var citations = bibEntryRelationsRepository.readCitations(bibEntry); // THEN Assertions.assertFalse(citations.isEmpty()); @@ -110,15 +110,15 @@ void givenAnEmptyCacheAndAValidBibEntryWhenReadingCitationsShouldPopulateTheCach var citationFetcherMock = new CitationFetcherMock( BibEntryRelationsRepositoryTest::getCitedBy, null, null ); - var bibEntryRelationsCache = new BibEntryRelationsCache(); - var bibEntryRelationsRepository = new BibEntryRelationsRepository( + var bibEntryRelationsCache = new LRUBibEntryRelationsCache(); + var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( citationFetcherMock, bibEntryRelationsCache ); var bibEntry = BibEntryRelationsRepositoryTest.createBibEntry(1); // WHEN Assertions.assertEquals(List.of(), bibEntryRelationsCache.getCitations(bibEntry)); - var citations = bibEntryRelationsRepository.getCitations(bibEntry); + var citations = bibEntryRelationsRepository.readCitations(bibEntry); var fromCacheCitations = bibEntryRelationsCache.getCitations(bibEntry); // THEN diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java new file mode 100644 index 00000000000..12b0a392944 --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java @@ -0,0 +1,39 @@ +package org.jabref.logic.citation.repository; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.jabref.model.entry.BibEntry; + +public class BibEntryRelationsRepositoryTestHelpers { + public static class CreateRepository { + public static BibEntryRelationsRepository from( + Function> retrieveCitations, + Function> retrieveReferences, + Consumer forceRefreshCitations, + Consumer forceRefreshReferences + ) { + return new BibEntryRelationsRepository() { + @Override + public List readCitations(BibEntry entry) { + return retrieveCitations.apply(entry); + } + + @Override + public List readReferences(BibEntry entry) { + return retrieveReferences.apply(entry); + } + + @Override + public void forceRefreshCitations(BibEntry entry) { + forceRefreshCitations.accept(entry); + } + + @Override + public void forceRefreshReferences(BibEntry entry) { + forceRefreshReferences.accept(entry); + } + }; + } + } +} diff --git a/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java new file mode 100644 index 00000000000..61473e8fd05 --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java @@ -0,0 +1,89 @@ +package org.jabref.logic.citation.service; + +import java.util.ArrayList; +import java.util.List; +import org.jabref.logic.citation.repository.BibEntryRelationsRepositoryTestHelpers; +import org.jabref.model.entry.BibEntry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SearchCitationsRelationsServiceTest { + + @Test + void serviceShouldSearchForReferences() { + // GIVEN + var referencesToReturn = List.of(new BibEntry()); + var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( + List::of, e -> referencesToReturn, e -> {}, e -> {} + ); + var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); + + // WHEN + var referencer = new BibEntry(); + List references = searchCitationsRelationsService.searchReferences(referencer); + + // THEN + Assertions.assertEquals(referencesToReturn, references); + } + + @Test + void serviceShouldForceReferencesUpdate() { + // GiVEN + var newReference = new BibEntry(); + var referencesToReturn = List.of(newReference); + var referenceToUpdate = new ArrayList(); + var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( + List::of, e -> referencesToReturn, e -> {}, e -> referenceToUpdate.add(newReference) + ); + var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); + + // WHEN + var referencer = new BibEntry(); + var references = searchCitationsRelationsService.searchReferences(referencer, true); + + // THEN + Assertions.assertEquals(referencesToReturn, references); + Assertions.assertEquals(1, referenceToUpdate.size()); + Assertions.assertSame(newReference, referenceToUpdate.getFirst()); + Assertions.assertNotSame(referencesToReturn, referenceToUpdate); + } + + @Test + void serviceShouldSearchForCitations() { + // GIVEN + var citationsToReturn = List.of(new BibEntry()); + var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( + e -> citationsToReturn, List::of, e -> {}, e -> {} + ); + var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); + + // WHEN + var cited = new BibEntry(); + List citations = searchCitationsRelationsService.searchCitations(cited); + + // THEN + Assertions.assertEquals(citationsToReturn, citations); + } + + @Test + void serviceShouldForceCitationsUpdate() { + // GiVEN + var newCitations = new BibEntry(); + var citationsToReturn = List.of(newCitations); + var citationsToUpdate = new ArrayList(); + var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( + e -> citationsToReturn, List::of, e -> citationsToUpdate.add(newCitations), e -> {} + ); + var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); + + // WHEN + var cited = new BibEntry(); + var citations = searchCitationsRelationsService.searchCitations(cited, true); + + // THEN + Assertions.assertEquals(citationsToReturn, citations); + Assertions.assertEquals(1, citationsToUpdate.size()); + Assertions.assertSame(newCitations, citationsToUpdate.getFirst()); + Assertions.assertNotSame(citationsToReturn, citationsToUpdate); + } +} From 18db75ea96874ce8bb5cdb1c999b3a0ab23ce907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Sat, 28 Sep 2024 21:55:32 +0200 Subject: [PATCH 03/10] Refactor Citations Relations Service Layer (#11189) * Move logic from repository to service * Refactor repositories * Update tab configuration --- .../CitationRelationsTab.java | 93 ++++--- .../CitationsRelationsTabViewModel.java | 2 +- .../BibEntryRelationsRepository.java | 24 +- .../repository/LRUBibEntryRelationsCache.java | 39 ++- .../LRUBibEntryRelationsRepository.java | 69 ++--- .../SearchCitationsRelationsService.java | 56 ++-- .../SemanticScholarCitationFetcher.java | 4 +- .../org/jabref/logic/util/BackgroundTask.java | 10 + .../CitationsRelationsTabViewModelTest.java | 3 +- ...ntryRelationsRepositoryHelpersForTest.java | 87 +++++++ .../BibEntryRelationsRepositoryTest.java | 128 ---------- ...ibEntryRelationsRepositoryTestHelpers.java | 39 --- .../LRUBibEntryRelationsRepositoryTest.java | 98 +++++++ .../SearchCitationsRelationsServiceTest.java | 239 ++++++++++++------ .../CitationFetcherHelpersForTest.java | 32 +++ 15 files changed, 552 insertions(+), 371 deletions(-) create mode 100644 src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java delete mode 100644 src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java delete mode 100644 src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java create mode 100644 src/test/java/org/jabref/logic/importer/fetcher/CitationFetcherHelpersForTest.java 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 048d058462d..e0f6c6c49a7 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -30,15 +30,16 @@ import org.jabref.gui.StateManager; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.entryeditor.EntryEditorTab; -import org.jabref.logic.citation.repository.BibEntryRelationsCache; -import org.jabref.logic.citation.repository.BibEntryRelationsRepository; -import org.jabref.logic.importer.fetcher.CitationFetcher; -import org.jabref.logic.importer.fetcher.SemanticScholarCitationFetcher; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.util.NoSelectionModel; import org.jabref.gui.util.ViewModelListCellFactory; +import org.jabref.logic.citation.repository.LRUBibEntryRelationsCache; +import org.jabref.logic.citation.repository.LRUBibEntryRelationsRepository; +import org.jabref.logic.citation.service.SearchCitationsRelationsService; import org.jabref.logic.database.DuplicateCheck; +import org.jabref.logic.importer.fetcher.CitationFetcher; +import org.jabref.logic.importer.fetcher.SemanticScholarCitationFetcher; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.BackgroundTask; import org.jabref.logic.util.TaskExecutor; @@ -73,7 +74,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; @@ -94,11 +95,22 @@ public CitationRelationsTab(DialogService dialogService, setTooltip(new Tooltip(Localization.lang("Show articles related by citation"))); this.duplicateCheck = new DuplicateCheck(new BibEntryTypesManager()); - this.bibEntryRelationsRepository = new BibEntryRelationsRepository( + var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( + new LRUBibEntryRelationsCache() + ); + this.searchCitationsRelationsService = new SearchCitationsRelationsService( new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), - new BibEntryRelationsCache() + bibEntryRelationsRepository + ); + citationsRelationsTabViewModel = new CitationsRelationsTabViewModel( + databaseContext, + preferences, + undoManager, + stateManager, + dialogService, + fileUpdateMonitor, + taskExecutor ); - citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(databaseContext, preferences, undoManager, stateManager, dialogService, fileUpdateMonitor, taskExecutor); } /** @@ -347,41 +359,54 @@ 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()))); - + listView.setPlaceholder( + new Label(Localization.lang( + "Error while fetching citing entries: %0", exception.getMessage()) + ) + ); refreshButton.setVisible(true); dialogService.notify(exception.getMessage()); }) .executeWith(taskExecutor); } + private BackgroundTask> 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, 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 0d0d5646ff1..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.logic.importer.fetcher.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/repository/BibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java index 48f38e2a38d..84b4c73a1d7 100644 --- a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java @@ -1,26 +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); - /** - * Fetch citations for a bib entry and update local database. - * @param entry should not be null - * @deprecated fetching citations should be done by the service layer (calling code) - */ - @Deprecated - void forceRefreshCitations(BibEntry entry); - - /** - * Fetch references made by a bib entry and update local database. - * @param entry should not be null - * @deprecated fetching references should be done by the service layer (calling code) - */ - @Deprecated - void forceRefreshReferences(BibEntry entry); + boolean containsReferences(BibEntry 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 index 8e6d491ce75..f87455fc033 100644 --- a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java +++ b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java @@ -1,38 +1,57 @@ package org.jabref.logic.citation.repository; -import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import org.eclipse.jgit.util.LRUMap; +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); + 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()); + return entry + .getDOI() + .stream() + .flatMap(doi -> CITATIONS_MAP.getOrDefault(doi, Set.of()).stream()) + .toList(); } public List getReferences(BibEntry entry) { - return REFERENCES_MAP.getOrDefault(entry.getDOI().map(DOI::getDOI).orElse(""), Collections.emptyList()); + 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 -> CITATIONS_MAP.put(doi.getDOI(), 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 -> REFERENCES_MAP.putIfAbsent(doi.getDOI(), 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 CITATIONS_MAP.containsKey(entry.getDOI().map(DOI::getDOI).orElse("")); + return entry.getDOI().map(CITATIONS_MAP::containsKey).orElse(false); } public boolean referencesCached(BibEntry entry) { - return REFERENCES_MAP.containsKey(entry.getDOI().map(DOI::getDOI).orElse("")); + return entry.getDOI().map(REFERENCES_MAP::containsKey).orElse(false); } } diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java index 23ab083ee4b..18c360ec17e 100644 --- a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java @@ -1,78 +1,49 @@ package org.jabref.logic.citation.repository; import java.util.List; +import java.util.Objects; -import org.jabref.logic.importer.fetcher.CitationFetcher; -import org.jabref.logic.importer.FetcherException; import org.jabref.model.entry.BibEntry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LRUBibEntryRelationsRepository implements BibEntryRelationsRepository { - private static final Logger LOGGER = LoggerFactory - .getLogger(LRUBibEntryRelationsRepository.class); - private final CitationFetcher fetcher; private final LRUBibEntryRelationsCache cache; - public LRUBibEntryRelationsRepository(CitationFetcher fetcher, LRUBibEntryRelationsCache cache) { - this.fetcher = fetcher; + public LRUBibEntryRelationsRepository(LRUBibEntryRelationsCache cache) { this.cache = cache; } @Override - public List readCitations(BibEntry entry) { - if (needToRefreshCitations(entry)) { - forceRefreshCitations(entry); - } - - return cache.getCitations(entry); + public void insertCitations(BibEntry entry, List citations) { + cache.cacheOrMergeCitations( + entry, Objects.requireNonNullElseGet(citations, List::of) + ); } @Override - public List readReferences(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 List readCitations(BibEntry entry) { + return cache.getCitations(entry); } @Override - 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 containsCitations(BibEntry entry) { + return cache.citationsCached(entry); } - private boolean needToRefreshCitations(BibEntry entry) { - return !cache.citationsCached(entry); + @Override + public void insertReferences(BibEntry entry, List references) { + cache.cacheOrMergeReferences( + entry, Objects.requireNonNullElseGet(references, List::of) + ); } - private boolean needToRefreshReferences(BibEntry entry) { - return !cache.referencesCached(entry); + @Override + public List readReferences(BibEntry entry) { + return cache.getReferences(entry); } @Override - 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); + public boolean containsReferences(BibEntry entry) { + return cache.referencesCached(entry); } } diff --git a/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java index 210821d708d..8dfc6ac5659 100644 --- a/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java +++ b/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java @@ -1,36 +1,60 @@ package org.jabref.logic.citation.service; import java.util.List; + import org.jabref.logic.citation.repository.BibEntryRelationsRepository; +import org.jabref.logic.importer.fetcher.CitationFetcher; import org.jabref.model.entry.BibEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class SearchCitationsRelationsService { - BibEntryRelationsRepository relationsRepository; + private static final Logger LOGGER = LoggerFactory + .getLogger(SearchCitationsRelationsService.class); - public SearchCitationsRelationsService(BibEntryRelationsRepository repository) { - this.relationsRepository = repository; - } + private final CitationFetcher citationFetcher; + private final BibEntryRelationsRepository relationsRepository; - public List searchReferences(BibEntry referencer) { - return this.relationsRepository.readReferences(referencer); + public SearchCitationsRelationsService( + CitationFetcher citationFetcher, BibEntryRelationsRepository repository + ) { + this.citationFetcher = citationFetcher; + this.relationsRepository = repository; } public List searchReferences(BibEntry referencer, boolean forceUpdate) { - if (forceUpdate) { - this.relationsRepository.forceRefreshReferences(referencer); + if (forceUpdate || !this.relationsRepository.containsReferences(referencer)) { + try { + var references = this.citationFetcher.searchCiting(referencer); + if (!references.isEmpty()) { + this.relationsRepository.insertReferences(referencer, references); + } + } catch (Exception e) { + var errMsg = "Error while fetching references for entry %s".formatted( + referencer.getTitle() + ); + LOGGER.error(errMsg); + } } - return this.searchReferences(referencer); - } - - public List searchCitations(BibEntry cited) { - return this.relationsRepository.readCitations(cited); + return this.relationsRepository.readReferences(referencer); } public List searchCitations(BibEntry cited, boolean forceUpdate) { - if (forceUpdate) { - this.relationsRepository.forceRefreshCitations(cited); + if (forceUpdate || !this.relationsRepository.containsCitations(cited)) { + try { + var citations = this.citationFetcher.searchCitedBy(cited); + if (!citations.isEmpty()) { + this.relationsRepository.insertCitations(cited, citations); + } + } catch (Exception e) { + var errMsg = "Error while fetching citations for entry %s".formatted( + cited.getTitle() + ); + LOGGER.error(errMsg); + } } - return this.searchCitations(cited); + return this.relationsRepository.readCitations(cited); } } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java index 51a4762432d..5efc66255ee 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/SemanticScholarCitationFetcher.java @@ -9,11 +9,11 @@ import org.jabref.logic.importer.ImporterPreferences; 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; -import org.jabref.model.citation.semanticscholar.CitationsResponse; -import org.jabref.model.citation.semanticscholar.ReferencesResponse; public class SemanticScholarCitationFetcher implements CitationFetcher, CustomizableKeyFetcher { private static final String SEMANTIC_SCHOLAR_API = "https://api.semanticscholar.org/graph/v1/"; 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/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java index 48e074fd00c..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.logic.importer.fetcher.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; 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/BibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java deleted file mode 100644 index 0b436ac9187..00000000000 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.jabref.logic.citation.repository; - -import java.util.HashSet; -import java.util.List; - -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import org.jabref.logic.importer.FetcherException; -import org.jabref.logic.importer.fetcher.CitationFetcher; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -class BibEntryRelationsRepositoryTest { - - private static Stream createBibEntries() { - return IntStream - .range(0, 150) - .mapToObj(BibEntryRelationsRepositoryTest::createBibEntry); - } - - private static List getCitedBy(BibEntry entry) { - return List.of(BibEntryRelationsRepositoryTest.createCitingBibEntry(entry)); - } - - private static BibEntry createBibEntry(int i) { - return new BibEntry() - .withCitationKey("entry" + i) - .withField(StandardField.DOI, "10.1234/5678" + i); - } - - private static BibEntry createCitingBibEntry(Integer i) { - return new BibEntry() - .withCitationKey("citing_entry" + i) - .withField(StandardField.DOI, "10.2345/6789" + i); - } - - private static BibEntry createCitingBibEntry(BibEntry citedEntry) { - return createCitingBibEntry( - Integer.valueOf(citedEntry.getCitationKey().orElseThrow().substring(5)) - ); - } - - /** - * Simple mock to avoid using Mockito (reduce overall complexity) - */ - private record CitationFetcherMock( - Function> searchCiteByDelegate, - Function> searchCitingDelegate, - String name - ) implements CitationFetcher { - - @Override - public List searchCitedBy(BibEntry entry) throws FetcherException { - return this.searchCiteByDelegate.apply(entry); - } - - @Override - public List searchCiting(BibEntry entry) throws FetcherException { - return this.searchCitingDelegate.apply(entry); - } - - @Override - public String getName() { - return this.name; - } - } - - @ParameterizedTest - @MethodSource("createBibEntries") - @DisplayName( - "Given a new bib entry when reading citations for it should call the fetcher" - ) - void givenANewEntryWhenReadingCitationsForItShouldCallTheFetcher(BibEntry bibEntry) { - // GIVEN - var entryCaptor = new HashSet(); - var citationFetcherMock = new CitationFetcherMock( - entry -> { - entryCaptor.add(entry); - return BibEntryRelationsRepositoryTest.getCitedBy(entry); - }, - null, - null - ); - var bibEntryRelationsCache = new LRUBibEntryRelationsCache(); - var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( - citationFetcherMock, bibEntryRelationsCache - ); - - // WHEN - var citations = bibEntryRelationsRepository.readCitations(bibEntry); - - // THEN - Assertions.assertFalse(citations.isEmpty()); - Assertions.assertTrue(entryCaptor.contains(bibEntry)); - } - - @Test - @DisplayName( - "Given an empty cache for a valid entry when reading the citations should populate cache" - ) - void givenAnEmptyCacheAndAValidBibEntryWhenReadingCitationsShouldPopulateTheCache() { - // GIVEN - var citationFetcherMock = new CitationFetcherMock( - BibEntryRelationsRepositoryTest::getCitedBy, null, null - ); - var bibEntryRelationsCache = new LRUBibEntryRelationsCache(); - var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( - citationFetcherMock, bibEntryRelationsCache - ); - var bibEntry = BibEntryRelationsRepositoryTest.createBibEntry(1); - - // WHEN - Assertions.assertEquals(List.of(), bibEntryRelationsCache.getCitations(bibEntry)); - var citations = bibEntryRelationsRepository.readCitations(bibEntry); - var fromCacheCitations = bibEntryRelationsCache.getCitations(bibEntry); - - // THEN - Assertions.assertFalse(fromCacheCitations.isEmpty()); - Assertions.assertEquals(citations, fromCacheCitations); - } -} diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java deleted file mode 100644 index 12b0a392944..00000000000 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryTestHelpers.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.jabref.logic.citation.repository; - -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import org.jabref.model.entry.BibEntry; - -public class BibEntryRelationsRepositoryTestHelpers { - public static class CreateRepository { - public static BibEntryRelationsRepository from( - Function> retrieveCitations, - Function> retrieveReferences, - Consumer forceRefreshCitations, - Consumer forceRefreshReferences - ) { - return new BibEntryRelationsRepository() { - @Override - public List readCitations(BibEntry entry) { - return retrieveCitations.apply(entry); - } - - @Override - public List readReferences(BibEntry entry) { - return retrieveReferences.apply(entry); - } - - @Override - public void forceRefreshCitations(BibEntry entry) { - forceRefreshCitations.accept(entry); - } - - @Override - public void forceRefreshReferences(BibEntry entry) { - forceRefreshReferences.accept(entry); - } - }; - } - } -} diff --git a/src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java new file mode 100644 index 00000000000..ee68782e2c9 --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java @@ -0,0 +1,98 @@ +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.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 LRUBibEntryRelationsRepositoryTest { + + private static Stream createBibEntries() { + return IntStream + .range(0, 150) + .mapToObj(LRUBibEntryRelationsRepositoryTest::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) { + // GIVEN + var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( + new LRUBibEntryRelationsCache() + ); + 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) { + // GIVEN + var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( + new LRUBibEntryRelationsCache() + ); + 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/service/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java index 61473e8fd05..32d8dea0b15 100644 --- a/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java +++ b/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java @@ -1,89 +1,178 @@ package org.jabref.logic.citation.service; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import org.jabref.logic.citation.repository.BibEntryRelationsRepositoryTestHelpers; + +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.Assertions; + +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 { - @Test - void serviceShouldSearchForReferences() { - // GIVEN - var referencesToReturn = List.of(new BibEntry()); - var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( - List::of, e -> referencesToReturn, e -> {}, e -> {} - ); - var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); - - // WHEN - var referencer = new BibEntry(); - List references = searchCitationsRelationsService.searchReferences(referencer); - - // THEN - Assertions.assertEquals(referencesToReturn, references); - } + @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); - @Test - void serviceShouldForceReferencesUpdate() { - // GiVEN - var newReference = new BibEntry(); - var referencesToReturn = List.of(newReference); - var referenceToUpdate = new ArrayList(); - var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( - List::of, e -> referencesToReturn, e -> {}, e -> referenceToUpdate.add(newReference) - ); - var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); - - // WHEN - var referencer = new BibEntry(); - var references = searchCitationsRelationsService.searchReferences(referencer, true); - - // THEN - Assertions.assertEquals(referencesToReturn, references); - Assertions.assertEquals(1, referenceToUpdate.size()); - Assertions.assertSame(newReference, referenceToUpdate.getFirst()); - Assertions.assertNotSame(referencesToReturn, referenceToUpdate); - } + // 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); - @Test - void serviceShouldSearchForCitations() { - // GIVEN - var citationsToReturn = List.of(new BibEntry()); - var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( - e -> citationsToReturn, List::of, e -> {}, e -> {} - ); - var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); - - // WHEN - var cited = new BibEntry(); - List citations = searchCitationsRelationsService.searchCitations(cited); - - // THEN - Assertions.assertEquals(citationsToReturn, citations); + // WHEN + var citations = searchService.searchCitations(cited, false); + + // THEN + assertTrue(citationsDatabase.containsKey(cited)); + assertEquals(citationsToReturn, citationsDatabase.get(cited)); + assertEquals(citationsToReturn, citations); + } } - @Test - void serviceShouldForceCitationsUpdate() { - // GiVEN - var newCitations = new BibEntry(); - var citationsToReturn = List.of(newCitations); - var citationsToUpdate = new ArrayList(); - var repository = BibEntryRelationsRepositoryTestHelpers.CreateRepository.from( - e -> citationsToReturn, List::of, e -> citationsToUpdate.add(newCitations), e -> {} - ); - var searchCitationsRelationsService = new SearchCitationsRelationsService(repository); - - // WHEN - var cited = new BibEntry(); - var citations = searchCitationsRelationsService.searchCitations(cited, true); - - // THEN - Assertions.assertEquals(citationsToReturn, citations); - Assertions.assertEquals(1, citationsToUpdate.size()); - Assertions.assertSame(newCitations, citationsToUpdate.getFirst()); - Assertions.assertNotSame(citationsToReturn, citationsToUpdate); + @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/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"; + } + }; + } + } +} From 9a31735a65d6d83f13d5bdc54ca10e7951017228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Wed, 6 Nov 2024 22:24:16 +0100 Subject: [PATCH 04/10] Address PR comments (#11901) --- .../CitationRelationsTab.java | 16 ++++++++-------- .../SearchCitationsRelationsService.java | 17 ++++++----------- .../SearchCitationsRelationsServiceTest.java | 1 + 3 files changed, 15 insertions(+), 19 deletions(-) rename src/main/java/org/jabref/logic/citation/{service => }/SearchCitationsRelationsService.java (78%) rename src/test/java/org/jabref/logic/citation/{service => }/SearchCitationsRelationsServiceTest.java (99%) 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 1b36d953ee7..e4af0a3fb5e 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -45,7 +45,7 @@ import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.citation.repository.LRUBibEntryRelationsCache; import org.jabref.logic.citation.repository.LRUBibEntryRelationsRepository; -import org.jabref.logic.citation.service.SearchCitationsRelationsService; +import org.jabref.logic.citation.SearchCitationsRelationsService; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.exporter.BibWriter; import org.jabref.logic.importer.fetcher.CitationFetcher; @@ -116,8 +116,7 @@ public CitationRelationsTab(DialogService dialogService, new LRUBibEntryRelationsCache() ); this.searchCitationsRelationsService = new SearchCitationsRelationsService( - new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), - bibEntryRelationsRepository + new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), bibEntryRelationsRepository ); citationsRelationsTabViewModel = new CitationsRelationsTabViewModel( databaseContext, @@ -415,6 +414,7 @@ private void searchForRelations(BibEntry entry, CheckListView { LOGGER.error("Error while fetching citing Articles", exception); hideNodes(abortButton, progress, importButton); - listView.setPlaceholder( - new Label(Localization.lang( - "Error while fetching citing entries: %0", exception.getMessage()) - ) - ); + listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", + exception.getMessage()))); refreshButton.setVisible(true); dialogService.notify(exception.getMessage()); }) .executeWith(taskExecutor); } + /** + * 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 ) { diff --git a/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java similarity index 78% rename from src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java rename to src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java index 8dfc6ac5659..11d041abb7f 100644 --- a/src/main/java/org/jabref/logic/citation/service/SearchCitationsRelationsService.java +++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java @@ -1,8 +1,9 @@ -package org.jabref.logic.citation.service; +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; @@ -31,11 +32,8 @@ public List searchReferences(BibEntry referencer, boolean forceUpdate) if (!references.isEmpty()) { this.relationsRepository.insertReferences(referencer, references); } - } catch (Exception e) { - var errMsg = "Error while fetching references for entry %s".formatted( - referencer.getTitle() - ); - LOGGER.error(errMsg); + } catch (FetcherException e) { + LOGGER.error("Error while fetching references for entry {}", referencer.getTitle(), e); } } return this.relationsRepository.readReferences(referencer); @@ -48,11 +46,8 @@ public List searchCitations(BibEntry cited, boolean forceUpdate) { if (!citations.isEmpty()) { this.relationsRepository.insertCitations(cited, citations); } - } catch (Exception e) { - var errMsg = "Error while fetching citations for entry %s".formatted( - cited.getTitle() - ); - LOGGER.error(errMsg); + } catch (FetcherException e) { + LOGGER.error("Error while fetching citations for entry {}", cited.getTitle(), e); } } return this.relationsRepository.readCitations(cited); diff --git a/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java similarity index 99% rename from src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java rename to src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java index 32d8dea0b15..25097ed4b87 100644 --- a/src/test/java/org/jabref/logic/citation/service/SearchCitationsRelationsServiceTest.java +++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java @@ -3,6 +3,7 @@ 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; From b1133d01c36430a4514ff59679a43bf5acab315c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Mon, 11 Nov 2024 20:42:58 +0100 Subject: [PATCH 05/10] MVStore implementation for citations: first approach (wip for open discussion) --- .../MVStoreBibEntryRelationsRepository.java | 38 +++ .../java/org/jabref/model/entry/BibEntry.java | 8 +- ...VStoreBibEntryRelationsRepositoryTest.java | 274 ++++++++++++++++++ 3 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java diff --git a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java new file mode 100644 index 00000000000..6631363df28 --- /dev/null +++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java @@ -0,0 +1,38 @@ +package org.jabref.logic.citation.repository; + +import java.util.List; + +import org.jabref.model.entry.BibEntry; + +public class MVStoreBibEntryRelationsRepository implements BibEntryRelationsRepository { + + @Override + public void insertCitations(BibEntry entry, List citations) { + throw new UnsupportedOperationException(); + } + + @Override + public List readCitations(BibEntry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCitations(BibEntry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public void insertReferences(BibEntry entry, List citations) { + throw new UnsupportedOperationException(); + } + + @Override + public List readReferences(BibEntry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsReferences(BibEntry entry) { + throw new UnsupportedOperationException(); + } +} 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/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java new file mode 100644 index 00000000000..26b299f3bfc --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java @@ -0,0 +1,274 @@ +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 MVStoreBibEntryRelationsRepositoryTest { + + private static Stream createBibEntries() { + return IntStream + .range(0, 150) + .mapToObj(MVStoreBibEntryRelationsRepositoryTest::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); + } + } +} From 6a8b21baabf8e1a42ac1a42500d30e5199a18552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Sun, 17 Nov 2024 01:19:18 +0100 Subject: [PATCH 06/10] Introduce the DAO layer for relations (#11189): * Implement MVStore for relations as DAO * Implement LRUCache for relations as DAO --- .../repository/BibEntryRelationDAO.java | 14 ++ .../LRUCacheBibEntryRelationsDAO.java | 75 ++++++++ .../MVStoreBibEntryRelationDAO.java | 181 ++++++++++++++++++ .../MVStoreBibEntryRelationsRepository.java | 38 ---- .../LRUCacheBibEntryRelationsDAOTest.java | 114 +++++++++++ ...oreBibEntryRelationsRepositoryDAOTest.java | 119 ++++++++++++ ...ryRelationsRepositoryPrototypingTest.java} | 10 +- 7 files changed, 511 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java create mode 100644 src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java create mode 100644 src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java delete mode 100644 src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java rename src/test/java/org/jabref/logic/citation/repository/{MVStoreBibEntryRelationsRepositoryTest.java => MVStoreBibEntryRelationsRepositoryPrototypingTest.java} (97%) 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/LRUCacheBibEntryRelationsDAO.java b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java new file mode 100644 index 00000000000..996e7661ecc --- /dev/null +++ b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java @@ -0,0 +1,75 @@ +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 abstract class LRUCacheBibEntryRelationsDAO implements BibEntryRelationDAO { + + private static final Integer 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(); + } + + public static class LRUCacheBibEntryCitations extends LRUCacheBibEntryRelationsDAO { + + private final static LRUCacheBibEntryCitations CITATIONS_CACHE = new LRUCacheBibEntryCitations(); + + private LRUCacheBibEntryCitations() { + super(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)); + } + + public static LRUCacheBibEntryCitations getInstance() { + return CITATIONS_CACHE; + } + } + + public static class LRUCacheBibEntryReferences extends LRUCacheBibEntryRelationsDAO { + + private final static LRUCacheBibEntryReferences REFERENCES_CACHE = new LRUCacheBibEntryReferences(); + + private LRUCacheBibEntryReferences() { + super(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)); + } + + public static LRUCacheBibEntryReferences getInstance() { + return REFERENCES_CACHE; + } + } +} 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..7bba5251063 --- /dev/null +++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java @@ -0,0 +1,181 @@ +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.DataUtils; +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 Path path; + private final String mapName; + private final MVMap.Builder> mapConfiguration = + new MVMap.Builder>().valueType(new BibEntryHashSetSerializer()); + + MVStoreBibEntryRelationDAO(Path path, String mapName) { + this.path = Objects.requireNonNull(path); + this.mapName = mapName; + } + + @Override + public List getRelations(BibEntry entry) { + return entry + .getDOI() + .map(doi -> { + try (var store = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).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 = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).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 = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).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(), + 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]; + } + } + + 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 + public LinkedHashSet[] createStorage(int size) { + return new LinkedHashSet[size]; + } + } +} diff --git a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java deleted file mode 100644 index 6631363df28..00000000000 --- a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.jabref.logic.citation.repository; - -import java.util.List; - -import org.jabref.model.entry.BibEntry; - -public class MVStoreBibEntryRelationsRepository implements BibEntryRelationsRepository { - - @Override - public void insertCitations(BibEntry entry, List citations) { - throw new UnsupportedOperationException(); - } - - @Override - public List readCitations(BibEntry entry) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsCitations(BibEntry entry) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertReferences(BibEntry entry, List citations) { - throw new UnsupportedOperationException(); - } - - @Override - public List readReferences(BibEntry entry) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsReferences(BibEntry entry) { - throw new UnsupportedOperationException(); - } -} 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..9d71799da9c --- /dev/null +++ b/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java @@ -0,0 +1,114 @@ +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.LRUCacheBibEntryCitations.getInstance(), + LRUCacheBibEntryRelationsDAO.LRUCacheBibEntryReferences.getInstance() + ) + .flatMap(dao -> createBibEntries().map(entry -> Arguments.of(dao, entry))); + } + + @ParameterizedTest + @MethodSource("createCacheAndBibEntry") + void repositoryShouldMergeCitationsWhenInserting(LRUCacheBibEntryRelationsDAO dao, BibEntry entry) { + // GIVEN + 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/MVStoreBibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java similarity index 97% rename from src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java rename to src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java index 26b299f3bfc..8c7d6ced91f 100644 --- a/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryPrototypingTest.java @@ -31,12 +31,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -class MVStoreBibEntryRelationsRepositoryTest { +class MVStoreBibEntryRelationsRepositoryPrototypingTest { private static Stream createBibEntries() { return IntStream .range(0, 150) - .mapToObj(MVStoreBibEntryRelationsRepositoryTest::createBibEntry); + .mapToObj(MVStoreBibEntryRelationsRepositoryPrototypingTest::createBibEntry); } private static BibEntry createBibEntry(int i) { @@ -271,4 +271,10 @@ void IWouldLikeToSaveAndLoadCitationsForABibEntryFromAStore(@TempDir Path tempor Assertions.assertEquals(entry.getValue(), citations); } } + + @Test + void test() { + var s = Stream.of(null, "test", null).collect(Collectors.joining()); + System.out.println(s); + } } From 01f6da47bdc018c5905b2c11d91ef99d0ec502cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Sun, 17 Nov 2024 21:13:27 +0100 Subject: [PATCH 07/10] MVStoreDAO implementation for citations relations (#11189): * Solve task 1 * Implementation of a DAO chain: memory cache and MVStore * Persist citations as relations to disk after a fetch * Avoid fetching data if relations are available from MVStore * Avoid reading data from MVStore if available in memory * Consume less from network, minimize disk usage --- .../CitationRelationsTab.java | 27 ++- .../repository/ChainBibEntryRelationDAO.java | 53 ++++++ .../ChainBibEntryRelationsRepository.java | 56 ++++++ .../LRUBibEntryRelationsRepository.java | 49 ------ .../LRUCacheBibEntryRelationsDAO.java | 37 +--- .../MVStoreBibEntryRelationDAO.java | 30 +++- .../ChainBibEntryRelationDAOTest.java | 159 ++++++++++++++++++ ...ChainBibEntryRelationsRepositoryTest.java} | 21 +-- .../LRUCacheBibEntryRelationsDAOTest.java | 5 +- 9 files changed, 330 insertions(+), 107 deletions(-) create mode 100644 src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java create mode 100644 src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java delete mode 100644 src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java create mode 100644 src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java rename src/test/java/org/jabref/logic/citation/repository/{LRUBibEntryRelationsRepositoryTest.java => ChainBibEntryRelationsRepositoryTest.java} (83%) 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 e4af0a3fb5e..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; @@ -43,8 +47,7 @@ import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldPreferences; import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.citation.repository.LRUBibEntryRelationsCache; -import org.jabref.logic.citation.repository.LRUBibEntryRelationsRepository; +import org.jabref.logic.citation.repository.ChainBibEntryRelationsRepository; import org.jabref.logic.citation.SearchCitationsRelationsService; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.exporter.BibWriter; @@ -112,12 +115,20 @@ public CitationRelationsTab(DialogService dialogService, this.entryTypesManager = bibEntryTypesManager; this.duplicateCheck = new DuplicateCheck(entryTypesManager); - var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( - new LRUBibEntryRelationsCache() - ); - this.searchCitationsRelationsService = new SearchCitationsRelationsService( - new SemanticScholarCitationFetcher(preferences.getImporterPreferences()), bibEntryRelationsRepository - ); + + 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, 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/LRUBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java deleted file mode 100644 index 18c360ec17e..00000000000 --- a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepository.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.jabref.logic.citation.repository; - -import java.util.List; -import java.util.Objects; - -import org.jabref.model.entry.BibEntry; - -public class LRUBibEntryRelationsRepository implements BibEntryRelationsRepository { - - private final LRUBibEntryRelationsCache cache; - - public LRUBibEntryRelationsRepository(LRUBibEntryRelationsCache cache) { - this.cache = cache; - } - - @Override - public void insertCitations(BibEntry entry, List citations) { - cache.cacheOrMergeCitations( - entry, Objects.requireNonNullElseGet(citations, List::of) - ); - } - - @Override - public List readCitations(BibEntry entry) { - return cache.getCitations(entry); - } - - @Override - public boolean containsCitations(BibEntry entry) { - return cache.citationsCached(entry); - } - - @Override - public void insertReferences(BibEntry entry, List references) { - cache.cacheOrMergeReferences( - entry, Objects.requireNonNullElseGet(references, List::of) - ); - } - - @Override - public List readReferences(BibEntry entry) { - return cache.getReferences(entry); - } - - @Override - public boolean containsReferences(BibEntry entry) { - return cache.referencesCached(entry); - } -} diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java index 996e7661ecc..017b3b93cb2 100644 --- a/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java @@ -10,9 +10,16 @@ import org.eclipse.jgit.util.LRUMap; -public abstract class LRUCacheBibEntryRelationsDAO implements BibEntryRelationDAO { +import static org.jabref.logic.citation.repository.LRUCacheBibEntryRelationsDAO.Configuration.MAX_CACHED_ENTRIES; - private static final Integer MAX_CACHED_ENTRIES = 100; +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; @@ -46,30 +53,4 @@ public boolean containsKey(BibEntry entry) { public void clearEntries() { this.relationsMap.clear(); } - - public static class LRUCacheBibEntryCitations extends LRUCacheBibEntryRelationsDAO { - - private final static LRUCacheBibEntryCitations CITATIONS_CACHE = new LRUCacheBibEntryCitations(); - - private LRUCacheBibEntryCitations() { - super(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)); - } - - public static LRUCacheBibEntryCitations getInstance() { - return CITATIONS_CACHE; - } - } - - public static class LRUCacheBibEntryReferences extends LRUCacheBibEntryRelationsDAO { - - private final static LRUCacheBibEntryReferences REFERENCES_CACHE = new LRUCacheBibEntryReferences(); - - private LRUCacheBibEntryReferences() { - super(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)); - } - - public static LRUCacheBibEntryReferences getInstance() { - return REFERENCES_CACHE; - } - } } diff --git a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java index 7bba5251063..fe0ee41be64 100644 --- a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java @@ -15,7 +15,6 @@ import org.jabref.model.entry.identifier.DOI; import org.jabref.model.entry.types.StandardEntryType; -import org.h2.mvstore.DataUtils; import org.h2.mvstore.MVMap; import org.h2.mvstore.MVStore; import org.h2.mvstore.WriteBuffer; @@ -23,14 +22,14 @@ public class MVStoreBibEntryRelationDAO implements BibEntryRelationDAO { - private final Path path; 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.path = Objects.requireNonNull(path); this.mapName = mapName; + this.storeConfiguration = new MVStore.Builder().autoCommitDisabled().fileName(path.toAbsolutePath().toString()); } @Override @@ -38,7 +37,7 @@ public List getRelations(BibEntry entry) { return entry .getDOI() .map(doi -> { - try (var store = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).open()) { + try (var store = this.storeConfiguration.open()) { MVMap> relationsMap = store.openMap(mapName, mapConfiguration); return relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>()).stream().toList(); } @@ -49,7 +48,7 @@ public List getRelations(BibEntry entry) { @Override synchronized public void cacheOrMergeRelations(BibEntry entry, List relations) { entry.getDOI().ifPresent(doi -> { - try (var store = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).open()) { + try (var store = this.storeConfiguration.open()) { MVMap> relationsMap = store.openMap(mapName, mapConfiguration); var relationsAlreadyStored = relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>()); relationsAlreadyStored.addAll(relations); @@ -64,7 +63,7 @@ public boolean containsKey(BibEntry entry) { return entry .getDOI() .map(doi -> { - try (var store = new MVStore.Builder().fileName(path.toAbsolutePath().toString()).open()) { + try (var store = this.storeConfiguration.open()) { MVMap> relationsMap = store.openMap(mapName, mapConfiguration); return relationsMap.containsKey(doi.getDOI()); } @@ -82,7 +81,7 @@ private static String toString(BibEntry entry) { entry.getTitle().orElse("null"), entry.getField(StandardField.YEAR).orElse("null"), entry.getField(StandardField.AUTHOR).orElse("null"), - entry.getType().getDisplayName(), + 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") @@ -124,8 +123,9 @@ public void write(WriteBuffer buff, BibEntry bibEntry) { @Override public BibEntry read(ByteBuffer buff) { int serializedEntrySize = buff.getInt(); - var serializedEntry = DataUtils.readString(buff, serializedEntrySize); - return fromString(serializedEntry); + var serializedEntry = new byte[serializedEntrySize]; + buff.get(serializedEntry); + return fromString(new String(serializedEntry, StandardCharsets.UTF_8)); } @Override @@ -140,6 +140,11 @@ public int compare(BibEntry a, BibEntry b) { public BibEntry[] createStorage(int size) { return new BibEntry[size]; } + + @Override + public boolean isMemoryEstimationAllowed() { + return false; + } } private static class BibEntryHashSetSerializer extends BasicDataType> { @@ -149,6 +154,7 @@ private static class BibEntryHashSetSerializer extends BasicDataType read(ByteBuffer buff) { } @Override + @SuppressWarnings("unchecked") public LinkedHashSet[] createStorage(int size) { return new LinkedHashSet[size]; } + + @Override + public boolean isMemoryEstimationAllowed() { + return false; + } } } 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/LRUBibEntryRelationsRepositoryTest.java b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java similarity index 83% rename from src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java rename to src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java index ee68782e2c9..d8d3bf8fbad 100644 --- a/src/test/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsRepositoryTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepositoryTest.java @@ -1,5 +1,6 @@ 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; @@ -16,12 +17,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; -class LRUBibEntryRelationsRepositoryTest { +class ChainBibEntryRelationsRepositoryTest { private static Stream createBibEntries() { return IntStream .range(0, 150) - .mapToObj(LRUBibEntryRelationsRepositoryTest::createBibEntry); + .mapToObj(ChainBibEntryRelationsRepositoryTest::createBibEntry); } private static BibEntry createBibEntry(int i) { @@ -46,11 +47,11 @@ private static List createRelations(BibEntry entry) { @ParameterizedTest @MethodSource("createBibEntries") - void repositoryShouldMergeCitationsWhenInserting(BibEntry bibEntry) { + void repositoryShouldMergeCitationsWhenInserting(BibEntry bibEntry) throws Exception { // GIVEN - var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( - new LRUBibEntryRelationsCache() - ); + var tempDir = Files.createTempDirectory("temp"); + var mvStorePath = Files.createTempFile(tempDir, "cache", ""); + var bibEntryRelationsRepository = new ChainBibEntryRelationsRepository(mvStorePath, mvStorePath); assertFalse(bibEntryRelationsRepository.containsCitations(bibEntry)); // WHEN @@ -72,11 +73,11 @@ void repositoryShouldMergeCitationsWhenInserting(BibEntry bibEntry) { @ParameterizedTest @MethodSource("createBibEntries") - void repositoryShouldMergeReferencesWhenInserting(BibEntry bibEntry) { + void repositoryShouldMergeReferencesWhenInserting(BibEntry bibEntry) throws Exception { // GIVEN - var bibEntryRelationsRepository = new LRUBibEntryRelationsRepository( - new LRUBibEntryRelationsCache() - ); + var tempDir = Files.createTempDirectory("temp"); + var mvStorePath = Files.createTempFile(tempDir, "cache", ""); + var bibEntryRelationsRepository = new ChainBibEntryRelationsRepository(mvStorePath, mvStorePath); assertFalse(bibEntryRelationsRepository.containsReferences(bibEntry)); // WHEN diff --git a/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java index 9d71799da9c..409352a78b9 100644 --- a/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAOTest.java @@ -65,9 +65,7 @@ private static List createRelations(BibEntry entry) { private static Stream createCacheAndBibEntry() { return Stream - .of(LRUCacheBibEntryRelationsDAO.LRUCacheBibEntryCitations.getInstance(), - LRUCacheBibEntryRelationsDAO.LRUCacheBibEntryReferences.getInstance() - ) + .of(LRUCacheBibEntryRelationsDAO.CITATIONS, LRUCacheBibEntryRelationsDAO.REFERENCES) .flatMap(dao -> createBibEntries().map(entry -> Arguments.of(dao, entry))); } @@ -75,6 +73,7 @@ private static Stream createCacheAndBibEntry() { @MethodSource("createCacheAndBibEntry") void repositoryShouldMergeCitationsWhenInserting(LRUCacheBibEntryRelationsDAO dao, BibEntry entry) { // GIVEN + dao.clearEntries(); assertFalse(dao.containsKey(entry)); // WHEN From 337780d2597693ce1ddc7009a611f3b40c78b5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Sun, 24 Nov 2024 21:51:19 +0100 Subject: [PATCH 08/10] Implement a search lock mechanism for citations relations (#11189): * Solve part of task 2: make impossible to force a search on a BibEntry over a week since last insertion * The MVStoreDAO search lock is based on a timestamp map (doi -> lastInsertionDate) * All time computation are based on UTC * The LRU cache will always return true -> the computer could stay up during a week, leaving cache in memory --- .../SearchCitationsRelationsService.java | 6 +- .../repository/BibEntryRelationDAO.java | 4 ++ ...DAO.java => BibEntryRelationDAOChain.java} | 14 +++-- .../BibEntryRelationsRepository.java | 4 ++ .../ChainBibEntryRelationsRepository.java | 14 ++++- .../repository/LRUBibEntryRelationsCache.java | 57 ------------------ .../MVStoreBibEntryRelationDAO.java | 44 +++++++++++++- .../SearchCitationsRelationsServiceTest.java | 3 +- ...java => BibEntryRelationDAOChainTest.java} | 44 ++++++++++---- ...ntryRelationsRepositoryHelpersForTest.java | 20 +++++++ ...oreBibEntryRelationsRepositoryDAOTest.java | 59 +++++++++++++++++-- 11 files changed, 184 insertions(+), 85 deletions(-) rename src/main/java/org/jabref/logic/citation/repository/{ChainBibEntryRelationDAO.java => BibEntryRelationDAOChain.java} (74%) delete mode 100644 src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java rename src/test/java/org/jabref/logic/citation/repository/{ChainBibEntryRelationDAOTest.java => BibEntryRelationDAOChainTest.java} (79%) diff --git a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java index 11d041abb7f..7a51b686077 100644 --- a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java +++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java @@ -26,7 +26,8 @@ public SearchCitationsRelationsService( } public List searchReferences(BibEntry referencer, boolean forceUpdate) { - if (forceUpdate || !this.relationsRepository.containsReferences(referencer)) { + if ((forceUpdate && this.relationsRepository.isReferencesUpdatable(referencer)) + || !this.relationsRepository.containsReferences(referencer)) { try { var references = this.citationFetcher.searchCiting(referencer); if (!references.isEmpty()) { @@ -40,7 +41,8 @@ public List searchReferences(BibEntry referencer, boolean forceUpdate) } public List searchCitations(BibEntry cited, boolean forceUpdate) { - if (forceUpdate || !this.relationsRepository.containsCitations(cited)) { + if ((forceUpdate && this.relationsRepository.isCitationsUpdatable(cited)) + || !this.relationsRepository.containsCitations(cited)) { try { var citations = this.citationFetcher.searchCitedBy(cited); if (!citations.isEmpty()) { diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java index e56b08f4d3f..883a4566a3c 100644 --- a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAO.java @@ -11,4 +11,8 @@ public interface BibEntryRelationDAO { void cacheOrMergeRelations(BibEntry entry, List relations); boolean containsKey(BibEntry entry); + + default boolean isUpdatable(BibEntry entry) { + return true; + } } diff --git a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java similarity index 74% rename from src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java rename to src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java index 3b802f7cada..2445e13a6fc 100644 --- a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChain.java @@ -4,14 +4,14 @@ import org.jabref.model.entry.BibEntry; -public class ChainBibEntryRelationDAO implements BibEntryRelationDAO { +public class BibEntryRelationDAOChain implements BibEntryRelationDAO { - private static final BibEntryRelationDAO EMPTY = new ChainBibEntryRelationDAO(null, null); + private static final BibEntryRelationDAO EMPTY = new BibEntryRelationDAOChain(null, null); private final BibEntryRelationDAO current; private final BibEntryRelationDAO next; - ChainBibEntryRelationDAO(BibEntryRelationDAO current, BibEntryRelationDAO next) { + BibEntryRelationDAOChain(BibEntryRelationDAO current, BibEntryRelationDAO next) { this.current = current; this.next = next; } @@ -44,10 +44,16 @@ public boolean containsKey(BibEntry entry) { || (this.next != EMPTY && this.next.containsKey(entry)); } + @Override + public boolean isUpdatable(BibEntry entry) { + return this.current.isUpdatable(entry) + && (this.next == EMPTY || this.next.isUpdatable(entry)); + } + public static BibEntryRelationDAO of(BibEntryRelationDAO... dao) { return List.of(dao) .reversed() .stream() - .reduce(EMPTY, (acc, current) -> new ChainBibEntryRelationDAO(current, acc)); + .reduce(EMPTY, (acc, current) -> new BibEntryRelationDAOChain(current, acc)); } } diff --git a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java index 84b4c73a1d7..cb99b791cad 100644 --- a/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/BibEntryRelationsRepository.java @@ -12,9 +12,13 @@ public interface BibEntryRelationsRepository { boolean containsCitations(BibEntry entry); + boolean isCitationsUpdatable(BibEntry entry); + void insertReferences(BibEntry entry, List citations); List readReferences(BibEntry entry); boolean containsReferences(BibEntry entry); + + boolean isReferencesUpdatable(BibEntry entry); } diff --git a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java index 289b08edb8f..14d26308d98 100644 --- a/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java +++ b/src/main/java/org/jabref/logic/citation/repository/ChainBibEntryRelationsRepository.java @@ -12,10 +12,10 @@ public class ChainBibEntryRelationsRepository implements BibEntryRelationsReposi private final BibEntryRelationDAO referencesDao; public ChainBibEntryRelationsRepository(Path citationsStore, Path relationsStore) { - this.citationsDao = ChainBibEntryRelationDAO.of( + this.citationsDao = BibEntryRelationDAOChain.of( LRUCacheBibEntryRelationsDAO.CITATIONS, new MVStoreBibEntryRelationDAO(citationsStore, "citations") ); - this.referencesDao = ChainBibEntryRelationDAO.of( + this.referencesDao = BibEntryRelationDAOChain.of( LRUCacheBibEntryRelationsDAO.REFERENCES, new MVStoreBibEntryRelationDAO(relationsStore, "relations") ); } @@ -37,6 +37,11 @@ public boolean containsCitations(BibEntry entry) { return citationsDao.containsKey(entry); } + @Override + public boolean isCitationsUpdatable(BibEntry entry) { + return citationsDao.isUpdatable(entry); + } + @Override public void insertReferences(BibEntry entry, List references) { referencesDao.cacheOrMergeRelations( @@ -53,4 +58,9 @@ public List readReferences(BibEntry entry) { public boolean containsReferences(BibEntry entry) { return referencesDao.containsKey(entry); } + + @Override + public boolean isReferencesUpdatable(BibEntry entry) { + return referencesDao.isUpdatable(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 deleted file mode 100644 index f87455fc033..00000000000 --- a/src/main/java/org/jabref/logic/citation/repository/LRUBibEntryRelationsCache.java +++ /dev/null @@ -1,57 +0,0 @@ -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/MVStoreBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java index fe0ee41be64..8bf4f0473da 100644 --- a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java @@ -3,18 +3,22 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.time.Clock; +import java.time.ZoneId; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.time.LocalDateTime; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.DOI; import org.jabref.model.entry.types.StandardEntryType; +import com.google.common.annotations.VisibleForTesting; import org.h2.mvstore.MVMap; import org.h2.mvstore.MVStore; import org.h2.mvstore.WriteBuffer; @@ -22,13 +26,17 @@ public class MVStoreBibEntryRelationDAO implements BibEntryRelationDAO { + private final static ZoneId TIME_STAMP_ZONE_ID = ZoneId.of("UTC"); + private final String mapName; + private final String insertionTimeStampMapName; private final MVStore.Builder storeConfiguration; private final MVMap.Builder> mapConfiguration = new MVMap.Builder>().valueType(new BibEntryHashSetSerializer()); MVStoreBibEntryRelationDAO(Path path, String mapName) { this.mapName = mapName; + this.insertionTimeStampMapName = mapName + "-insertion-timestamp"; this.storeConfiguration = new MVStore.Builder().autoCommitDisabled().fileName(path.toAbsolutePath().toString()); } @@ -47,19 +55,30 @@ public List getRelations(BibEntry entry) { @Override synchronized public void cacheOrMergeRelations(BibEntry entry, List relations) { + if (relations.isEmpty()) { + return; + } entry.getDOI().ifPresent(doi -> { try (var store = this.storeConfiguration.open()) { + // Save the relations MVMap> relationsMap = store.openMap(mapName, mapConfiguration); var relationsAlreadyStored = relationsMap.getOrDefault(doi.getDOI(), new LinkedHashSet<>()); relationsAlreadyStored.addAll(relations); relationsMap.put(doi.getDOI(), relationsAlreadyStored); + + // Save insertion timestamp + var insertionTime = LocalDateTime.now(TIME_STAMP_ZONE_ID); + MVMap insertionTimeStampMap = store.openMap(insertionTimeStampMapName); + insertionTimeStampMap.put(doi.getDOI(), insertionTime); + + // Commit store.commit(); } }); } @Override - public boolean containsKey(BibEntry entry) { + synchronized public boolean containsKey(BibEntry entry) { return entry .getDOI() .map(doi -> { @@ -71,6 +90,29 @@ public boolean containsKey(BibEntry entry) { .orElse(false); } + @Override + synchronized public boolean isUpdatable(BibEntry entry) { + var clock = Clock.system(TIME_STAMP_ZONE_ID); + return this.isUpdatable(entry, clock); + } + + @VisibleForTesting + boolean isUpdatable(final BibEntry entry, final Clock clock) { + final var executionTime = LocalDateTime.now(clock); + return entry + .getDOI() + .map(doi -> { + try (var store = this.storeConfiguration.open()) { + MVMap insertionTimeStampMap = store.openMap(insertionTimeStampMapName); + return insertionTimeStampMap.getOrDefault(doi.getDOI(), executionTime); + } + }) + .map(lastExecutionTime -> + lastExecutionTime.equals(executionTime) || lastExecutionTime.isBefore(executionTime.minusWeeks(1)) + ) + .orElse(true); + } + private static class BibEntrySerializer extends BasicDataType { private final static String FIELD_SEPARATOR = "--"; diff --git a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java index 25097ed4b87..1d1f1fbbf8f 100644 --- a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java +++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java @@ -1,9 +1,8 @@ -package org.jabref.logic.citation.service; +package org.jabref.logic.citation; 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; diff --git a/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java similarity index 79% rename from src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java rename to src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java index 6507c9b1d80..d83156f0745 100644 --- a/src/test/java/org/jabref/logic/citation/repository/ChainBibEntryRelationDAOTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationDAOChainTest.java @@ -19,12 +19,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class ChainBibEntryRelationDAOTest { +class BibEntryRelationDAOChainTest { private static Stream createBibEntries() { return IntStream .range(0, 150) - .mapToObj(ChainBibEntryRelationDAOTest::createBibEntry); + .mapToObj(BibEntryRelationDAOChainTest::createBibEntry); } private static BibEntry createBibEntry(int i) { @@ -37,6 +37,7 @@ private static BibEntry createBibEntry(int 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 */ @@ -68,7 +69,7 @@ private static Stream createCacheAndBibEntry() { }); } - private class DaoMock implements BibEntryRelationDAO { + private static class DaoMock implements BibEntryRelationDAO { Map> table = new HashMap<>(); @@ -86,6 +87,11 @@ public void cacheOrMergeRelations(BibEntry entry, List relations) { public boolean containsKey(BibEntry entry) { return this.table.containsKey(entry); } + + @Override + public boolean isUpdatable(BibEntry entry) { + return !this.containsKey(entry); + } } @ParameterizedTest @@ -95,7 +101,7 @@ void theChainShouldReadFromFirstNode(BibEntryRelationDAO dao, BibEntry entry) { var relations = createRelations(entry); dao.cacheOrMergeRelations(entry, relations); var secondDao = new DaoMock(); - var doaChain = ChainBibEntryRelationDAO.of(dao, secondDao); + var doaChain = BibEntryRelationDAOChain.of(dao, secondDao); // WHEN var relationsFromChain = doaChain.getRelations(entry); @@ -112,7 +118,7 @@ void theChainShouldReadFromSecondNode(BibEntryRelationDAO dao, BibEntry entry) { var relations = createRelations(entry); dao.cacheOrMergeRelations(entry, relations); var firstDao = new DaoMock(); - var doaChain = ChainBibEntryRelationDAO.of(firstDao, dao); + var doaChain = BibEntryRelationDAOChain.of(firstDao, dao); // WHEN var relationsFromChain = doaChain.getRelations(entry); @@ -128,7 +134,7 @@ void theChainShouldReadFromSecondNodeAndRecopyToFirstNode(BibEntryRelationDAO da // GIVEN var relations = createRelations(entry); var firstDao = new DaoMock(); - var doaChain = ChainBibEntryRelationDAO.of(firstDao, dao); + var doaChain = BibEntryRelationDAOChain.of(firstDao, dao); // WHEN doaChain.cacheOrMergeRelations(entry, relations); @@ -142,18 +148,34 @@ void theChainShouldReadFromSecondNodeAndRecopyToFirstNode(BibEntryRelationDAO da @ParameterizedTest @MethodSource("createCacheAndBibEntry") - void theChainShouldContainAKeyEvenIfItWasOnlyInsertedInLastNode(BibEntryRelationDAO dao, BibEntry entry) { + void theChainShouldContainAKeyEvenIfItWasOnlyInsertedInLastNode(BibEntryRelationDAO secondDao, BibEntry entry) { // GIVEN var relations = createRelations(entry); var firstDao = new DaoMock(); - var doaChain = ChainBibEntryRelationDAO.of(firstDao, dao); + var doaChain = BibEntryRelationDAOChain.of(firstDao, secondDao); // WHEN - dao.cacheOrMergeRelations(entry, relations); + secondDao.cacheOrMergeRelations(entry, relations); + + // THEN Assertions.assertFalse(firstDao.containsKey(entry)); - boolean doesChainContainsTheKey = doaChain.containsKey(entry); + Assertions.assertTrue(doaChain.containsKey(entry)); + } + + @ParameterizedTest + @MethodSource("createCacheAndBibEntry") + void theChainShouldNotBeUpdatableBeforeInsertionAndNotAfterAnInsertion(BibEntryRelationDAO dao, BibEntry entry) { + // GIVEN + var relations = createRelations(entry); + var lastDao = new DaoMock(); + var daoChain = BibEntryRelationDAOChain.of(dao, lastDao); + Assertions.assertTrue(daoChain.isUpdatable(entry)); + + // WHEN + daoChain.cacheOrMergeRelations(entry, relations); // THEN - Assertions.assertTrue(doesChainContainsTheKey); + Assertions.assertTrue(daoChain.containsKey(entry)); + Assertions.assertFalse(daoChain.isUpdatable(entry)); } } diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java index f2305b38b33..c829ed188b7 100644 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java @@ -31,6 +31,11 @@ public boolean containsCitations(BibEntry entry) { return true; } + @Override + public boolean isCitationsUpdatable(BibEntry entry) { + return false; + } + @Override public void insertReferences(BibEntry entry, List citations) { insertReferences.accept(entry, citations); @@ -45,6 +50,11 @@ public List readReferences(BibEntry entry) { public boolean containsReferences(BibEntry entry) { return true; } + + @Override + public boolean isReferencesUpdatable(BibEntry entry) { + return false; + } }; } @@ -67,6 +77,11 @@ public boolean containsCitations(BibEntry entry) { return citationsDB.containsKey(entry); } + @Override + public boolean isCitationsUpdatable(BibEntry entry) { + return false; + } + @Override public void insertReferences(BibEntry entry, List citations) { referencesDB.put(entry, citations); @@ -81,6 +96,11 @@ public List readReferences(BibEntry entry) { public boolean containsReferences(BibEntry entry) { return referencesDB.containsKey(entry); } + + @Override + public boolean isReferencesUpdatable(BibEntry entry) { + return false; + } }; } } diff --git a/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java index ad6a175cfaf..1816ed10096 100644 --- a/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationsRepositoryDAOTest.java @@ -3,6 +3,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.List; import java.util.random.RandomGenerator; import java.util.stream.Collectors; @@ -26,6 +31,9 @@ class MVStoreBibEntryRelationsRepositoryDAOTest { + private final static String TEMPORARY_FOLDER_NAME = "bib_entry_relations_test_not_contains_store"; + private final static String MAP_NAME = "test-relations"; + @TempDir Path temporaryFolder; private static Stream createBibEntries() { @@ -44,6 +52,7 @@ private static BibEntry createBibEntry(int 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 */ @@ -70,8 +79,8 @@ private static List createRelations(BibEntry entry) { @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"); + var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME)); + var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME); Assertions.assertFalse(dao.containsKey(bibEntry)); var firstRelations = createRelations(bibEntry); var secondRelations = createRelations(bibEntry); @@ -95,8 +104,8 @@ void DAOShouldMergeRelationsWhenInserting(BibEntry bibEntry) throws IOException @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"); + var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME)); + var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME); // THEN Assertions.assertFalse(dao.containsKey(entry)); @@ -106,8 +115,8 @@ void containsKeyShouldReturnFalseIfNothingWasInserted(BibEntry entry) throws IOE @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 file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME)); + var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME); var relations = createRelations(entry); // WHEN @@ -116,4 +125,42 @@ void containsKeyShouldReturnTrueIfRelationsWereInserted(BibEntry entry) throws I // THEN Assertions.assertTrue(dao.containsKey(entry)); } + + @ParameterizedTest + @MethodSource("createBibEntries") + void isUpdatableShouldReturnTrueBeforeInsertionsAndFalseAfterInsertions(BibEntry entry) throws IOException { + // GIVEN + var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME)); + var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME); + var relations = createRelations(entry); + Assertions.assertTrue(dao.isUpdatable(entry)); + + // WHEN + dao.cacheOrMergeRelations(entry, relations); + + // THEN + Assertions.assertFalse(dao.isUpdatable(entry)); + } + + @ParameterizedTest + @MethodSource("createBibEntries") + void isUpdatableShouldReturnTrueAfterOneWeek(BibEntry entry) throws IOException { + // GIVEN + var file = Files.createFile(temporaryFolder.resolve(TEMPORARY_FOLDER_NAME)); + var dao = new MVStoreBibEntryRelationDAO(file.toAbsolutePath(), MAP_NAME); + var relations = createRelations(entry); + var clock = Clock.fixed(Instant.now(), ZoneId.of("UTC")); + Assertions.assertTrue(dao.isUpdatable(entry, clock)); + + // WHEN + dao.cacheOrMergeRelations(entry, relations); + + // THEN + Assertions.assertFalse(dao.isUpdatable(entry, clock)); + var clockOneWeekAfter = Clock.fixed( + LocalDateTime.now(ZoneId.of("UTC")).plusWeeks(1).toInstant(ZoneOffset.UTC), + ZoneId.of("UTC") + ); + Assertions.assertTrue(dao.isUpdatable(entry, clockOneWeekAfter)); + } } From 7c2e32d2e62be0635a83169646d2e7701211f721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Wed, 27 Nov 2024 22:52:28 +0100 Subject: [PATCH 09/10] Avoid user to force update citation relations even the fetcher returned an empty list * Solve completely Task 2: make impossible to force a search on a BibEntry over a week since last insertion --- .../SearchCitationsRelationsService.java | 8 +--- .../MVStoreBibEntryRelationDAO.java | 9 ++-- .../SearchCitationsRelationsServiceTest.java | 44 ++++++++++++++++++- ...ntryRelationsRepositoryHelpersForTest.java | 8 ++-- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java index 7a51b686077..30b655bdde1 100644 --- a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java +++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java @@ -30,9 +30,7 @@ public List searchReferences(BibEntry referencer, boolean forceUpdate) || !this.relationsRepository.containsReferences(referencer)) { try { var references = this.citationFetcher.searchCiting(referencer); - if (!references.isEmpty()) { - this.relationsRepository.insertReferences(referencer, references); - } + this.relationsRepository.insertReferences(referencer, references); } catch (FetcherException e) { LOGGER.error("Error while fetching references for entry {}", referencer.getTitle(), e); } @@ -45,9 +43,7 @@ public List searchCitations(BibEntry cited, boolean forceUpdate) { || !this.relationsRepository.containsCitations(cited)) { try { var citations = this.citationFetcher.searchCitedBy(cited); - if (!citations.isEmpty()) { - this.relationsRepository.insertCitations(cited, citations); - } + this.relationsRepository.insertCitations(cited, citations); } catch (FetcherException e) { LOGGER.error("Error while fetching citations for entry {}", cited.getTitle(), e); } diff --git a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java index 8bf4f0473da..dd46f6f654b 100644 --- a/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/MVStoreBibEntryRelationDAO.java @@ -53,11 +53,14 @@ public List getRelations(BibEntry entry) { .orElse(List.of()); } + /** + * Allows insertion of empty list in order to keep track of insertion date for an entry. + * + * @param entry should not be null + * @param relations should not be null + */ @Override synchronized public void cacheOrMergeRelations(BibEntry entry, List relations) { - if (relations.isEmpty()) { - return; - } entry.getDOI().ifPresent(doi -> { try (var store = this.storeConfiguration.open()) { // Save the relations diff --git a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java index 1d1f1fbbf8f..2e63294f667 100644 --- a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java +++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java @@ -95,6 +95,27 @@ void serviceShouldFetchCitationsIfRepositoryIsEmpty() { assertEquals(citationsToReturn, citationsDatabase.get(cited)); assertEquals(citationsToReturn, citations); } + + @Test + void insertingAnEmptyCitationsShouldBePossible() { + var cited = new BibEntry(); + var citationsDatabase = new HashMap>(); + var fetcher = CitationFetcherHelpersForTest.Mocks.from( + entry -> List.of(), null + ); + var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from( + citationsDatabase, null + ); + var searchService = new SearchCitationsRelationsService(fetcher, repository); + + // WHEN + var citations = searchService.searchCitations(cited, false); + + // THEN + assertTrue(citations.isEmpty()); + assertTrue(citationsDatabase.containsKey(cited)); + assertTrue(citationsDatabase.get(cited).isEmpty()); + } } @Nested @@ -117,7 +138,7 @@ void serviceShouldSearchForReferences() { } @Test - void serviceShouldCallTheFetcherForReferencesIWhenForceUpdateIsTrue() { + void serviceShouldCallTheFetcherForReferencesWhenForceUpdateIsTrue() { // GIVEN var referencer = new BibEntry(); var newReference = new BibEntry(); @@ -174,5 +195,26 @@ void serviceShouldFetchReferencesIfRepositoryIsEmpty() { assertEquals(referencesToReturn, referencesDatabase.get(reference)); assertEquals(referencesToReturn, references); } + + @Test + void insertingAnEmptyReferencesShouldBePossible() { + var referencer = new BibEntry(); + var referenceDatabase = new HashMap>(); + var fetcher = CitationFetcherHelpersForTest.Mocks.from( + null, entry -> List.of() + ); + var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from( + null, referenceDatabase + ); + var searchService = new SearchCitationsRelationsService(fetcher, repository); + + // WHEN + var citations = searchService.searchReferences(referencer, false); + + // THEN + assertTrue(citations.isEmpty()); + assertTrue(referenceDatabase.containsKey(referencer)); + assertTrue(referenceDatabase.get(referencer).isEmpty()); + } } } diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java index c829ed188b7..14087295e63 100644 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java @@ -33,7 +33,7 @@ public boolean containsCitations(BibEntry entry) { @Override public boolean isCitationsUpdatable(BibEntry entry) { - return false; + return true; } @Override @@ -53,7 +53,7 @@ public boolean containsReferences(BibEntry entry) { @Override public boolean isReferencesUpdatable(BibEntry entry) { - return false; + return true; } }; } @@ -79,7 +79,7 @@ public boolean containsCitations(BibEntry entry) { @Override public boolean isCitationsUpdatable(BibEntry entry) { - return false; + return true; } @Override @@ -99,7 +99,7 @@ public boolean containsReferences(BibEntry entry) { @Override public boolean isReferencesUpdatable(BibEntry entry) { - return false; + return true; } }; } From 187b5d435644995ea43196d90ada41451e66fb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Cr=C3=A9mieux?= Date: Sun, 8 Dec 2024 22:54:52 +0100 Subject: [PATCH 10/10] Make the citation relations update automatic after the guard delay is exhausted * Remove the isForceUpdate boolean * User is still able to trigger the fetch if an error occurs --- .../CitationRelationsTab.java | 19 ++++++----- .../SearchCitationsRelationsService.java | 14 ++++---- .../LRUCacheBibEntryRelationsDAO.java | 2 +- .../SearchCitationsRelationsServiceTest.java | 32 +++++++++++-------- ...ntryRelationsRepositoryHelpersForTest.java | 8 +++-- 5 files changed, 41 insertions(+), 34 deletions(-) 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 1ead7439e22..8761224d060 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -4,7 +4,6 @@ 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; @@ -213,11 +212,11 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { refreshCitingButton.setOnMouseClicked(event -> { searchForRelations(entry, citingListView, abortCitingButton, - refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true); + refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress); }); refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, - refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); + refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress)); // Create SplitPane to hold all nodes above SplitPane container = new SplitPane(citingVBox, citedByVBox); @@ -225,10 +224,10 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { styleFetchedListView(citingListView); searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, - CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); + CitationFetcher.SearchType.CITES, importCitingButton, citingProgress); searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, - CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); + CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress); return container; } @@ -411,7 +410,7 @@ protected void bindToEntry(BibEntry entry) { */ private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton, Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress, boolean shouldRefresh) { + ProgressIndicator progress) { if (entry.getDOI().isEmpty()) { hideNodes(abortButton, progress); showNodes(refreshButton); @@ -432,7 +431,7 @@ private void searchForRelations(BibEntry entry, CheckListView prepareToSearchForRelations( abortButton, refreshButton, importButton, progress, task )) @@ -462,18 +461,18 @@ private void searchForRelations(BibEntry entry, CheckListView> createBackGroundTask( - BibEntry entry, CitationFetcher.SearchType searchType, boolean shouldRefresh + BibEntry entry, CitationFetcher.SearchType searchType ) { return switch (searchType) { case CitationFetcher.SearchType.CITES -> { citingTask = BackgroundTask.wrap( - () -> this.searchCitationsRelationsService.searchReferences(entry, shouldRefresh) + () -> this.searchCitationsRelationsService.searchReferences(entry) ); yield citingTask; } case CitationFetcher.SearchType.CITED_BY -> { citedByTask = BackgroundTask.wrap( - () -> this.searchCitationsRelationsService.searchCitations(entry, shouldRefresh) + () -> this.searchCitationsRelationsService.searchCitations(entry) ); yield citedByTask; } diff --git a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java index 30b655bdde1..0e800ca1077 100644 --- a/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java +++ b/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java @@ -25,9 +25,10 @@ public SearchCitationsRelationsService( this.relationsRepository = repository; } - public List searchReferences(BibEntry referencer, boolean forceUpdate) { - if ((forceUpdate && this.relationsRepository.isReferencesUpdatable(referencer)) - || !this.relationsRepository.containsReferences(referencer)) { + public List searchReferences(BibEntry referencer) { + boolean isFetchingAllowed = this.relationsRepository.isReferencesUpdatable(referencer) + || !this.relationsRepository.containsReferences(referencer); + if (isFetchingAllowed) { try { var references = this.citationFetcher.searchCiting(referencer); this.relationsRepository.insertReferences(referencer, references); @@ -38,9 +39,10 @@ public List searchReferences(BibEntry referencer, boolean forceUpdate) return this.relationsRepository.readReferences(referencer); } - public List searchCitations(BibEntry cited, boolean forceUpdate) { - if ((forceUpdate && this.relationsRepository.isCitationsUpdatable(cited)) - || !this.relationsRepository.containsCitations(cited)) { + public List searchCitations(BibEntry cited) { + boolean isFetchingAllowed = this.relationsRepository.isCitationsUpdatable(cited) + || !this.relationsRepository.containsCitations(cited); + if (isFetchingAllowed) { try { var citations = this.citationFetcher.searchCitedBy(cited); this.relationsRepository.insertCitations(cited, citations); diff --git a/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java index 017b3b93cb2..3b753dc92c0 100644 --- a/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java +++ b/src/main/java/org/jabref/logic/citation/repository/LRUCacheBibEntryRelationsDAO.java @@ -18,7 +18,7 @@ public enum LRUCacheBibEntryRelationsDAO implements BibEntryRelationDAO { REFERENCES(new LRUMap<>(MAX_CACHED_ENTRIES, MAX_CACHED_ENTRIES)); public static class Configuration { - public static final int MAX_CACHED_ENTRIES = 100; + public static final int MAX_CACHED_ENTRIES = 128; // Let's use a power of two for sizing } private final Map> relationsMap; diff --git a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java index 2e63294f667..1e15af113f7 100644 --- a/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java +++ b/src/test/java/org/jabref/logic/citation/SearchCitationsRelationsServiceTest.java @@ -23,19 +23,19 @@ void serviceShouldSearchForCitations() { var cited = new BibEntry(); var citationsToReturn = List.of(new BibEntry()); var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from( - e -> citationsToReturn, null, null, null + e -> citationsToReturn, null, null, null, entry -> false, entry -> false ); var searchService = new SearchCitationsRelationsService(null, repository); // WHEN - List citations = searchService.searchCitations(cited, false); + List citations = searchService.searchCitations(cited); // THEN assertEquals(citationsToReturn, citations); } @Test - void serviceShouldForceCitationsUpdate() { + void serviceShouldCallTheFetcherForCitationsWhenRepositoryIsUpdatable() { // GiVEN var cited = new BibEntry(); var newCitations = new BibEntry(); @@ -54,12 +54,14 @@ void serviceShouldForceCitationsUpdate() { e -> citationsToReturn, citationsDatabase::put, List::of, - (e, r) -> { } + (e, r) -> { }, + e -> true, + e -> false ); var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var citations = searchService.searchCitations(cited, true); + var citations = searchService.searchCitations(cited); // THEN assertTrue(citationsDatabase.containsKey(cited)); @@ -88,7 +90,7 @@ void serviceShouldFetchCitationsIfRepositoryIsEmpty() { var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var citations = searchService.searchCitations(cited, false); + var citations = searchService.searchCitations(cited); // THEN assertTrue(citationsDatabase.containsKey(cited)); @@ -109,7 +111,7 @@ void insertingAnEmptyCitationsShouldBePossible() { var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var citations = searchService.searchCitations(cited, false); + var citations = searchService.searchCitations(cited); // THEN assertTrue(citations.isEmpty()); @@ -126,19 +128,19 @@ void serviceShouldSearchForReferences() { var referencer = new BibEntry(); var referencesToReturn = List.of(new BibEntry()); var repository = BibEntryRelationsRepositoryHelpersForTest.Mocks.from( - null, null, e -> referencesToReturn, null + null, null, e -> referencesToReturn, null, e -> false, e -> false ); var searchService = new SearchCitationsRelationsService(null, repository); // WHEN - List references = searchService.searchReferences(referencer, false); + List references = searchService.searchReferences(referencer); // THEN assertEquals(referencesToReturn, references); } @Test - void serviceShouldCallTheFetcherForReferencesWhenForceUpdateIsTrue() { + void serviceShouldCallTheFetcherForReferencesWhenRepositoryIsUpdatable() { // GIVEN var referencer = new BibEntry(); var newReference = new BibEntry(); @@ -154,12 +156,14 @@ void serviceShouldCallTheFetcherForReferencesWhenForceUpdateIsTrue() { List::of, (e, c) -> { }, e -> referencesToReturn, - referencesDatabase::put + referencesDatabase::put, + e -> false, + e -> true ); var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var references = searchService.searchReferences(referencer, true); + var references = searchService.searchReferences(referencer); // THEN assertTrue(referencesDatabase.containsKey(referencer)); @@ -188,7 +192,7 @@ void serviceShouldFetchReferencesIfRepositoryIsEmpty() { var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var references = searchService.searchReferences(reference, false); + var references = searchService.searchReferences(reference); // THEN assertTrue(referencesDatabase.containsKey(reference)); @@ -209,7 +213,7 @@ void insertingAnEmptyReferencesShouldBePossible() { var searchService = new SearchCitationsRelationsService(fetcher, repository); // WHEN - var citations = searchService.searchReferences(referencer, false); + var citations = searchService.searchReferences(referencer); // THEN assertTrue(citations.isEmpty()); diff --git a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java index 14087295e63..d9a9b4bc920 100644 --- a/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java +++ b/src/test/java/org/jabref/logic/citation/repository/BibEntryRelationsRepositoryHelpersForTest.java @@ -13,7 +13,9 @@ public static BibEntryRelationsRepository from( Function> retrieveCitations, BiConsumer> insertCitations, Function> retrieveReferences, - BiConsumer> insertReferences + BiConsumer> insertReferences, + Function isCitationsUpdatable, + Function isReferencesUpdatable ) { return new BibEntryRelationsRepository() { @Override @@ -33,7 +35,7 @@ public boolean containsCitations(BibEntry entry) { @Override public boolean isCitationsUpdatable(BibEntry entry) { - return true; + return isCitationsUpdatable.apply(entry); } @Override @@ -53,7 +55,7 @@ public boolean containsReferences(BibEntry entry) { @Override public boolean isReferencesUpdatable(BibEntry entry) { - return true; + return isReferencesUpdatable.apply(entry); } }; }