From bae698a58bd34db0ef5b146593e6d35d884143d4 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 8 Jan 2024 21:58:08 +0100 Subject: [PATCH] Add cites field to bib entries for citation relation (#10752) * Add cites field to bib entries for citation relation Change list view order Fixes https://github.com/JabRef/jabref-issue-melting-pot/issues/345 * add changelog fix l10n * remove * fix * add viewmodel and tests * fix checkstyle * fix * Minor updates ^^ --------- Co-authored-by: Oliver Kopp --- CHANGELOG.md | 3 + .../jabref/gui/entryeditor/EntryEditor.java | 2 +- .../CitationRelationItem.java | 19 +- .../CitationRelationsTab.java | 71 +++-- .../CitationsRelationsTabViewModel.java | 115 ++++++++ .../semanticscholar/AuthorResponse.java | 3 + .../semanticscholar/CitationDataItem.java | 3 + .../semanticscholar/CitationFetcher.java | 2 +- .../semanticscholar/CitationsResponse.java | 3 + .../semanticscholar/ReferenceDataItem.java | 7 +- .../semanticscholar/ReferencesResponse.java | 3 + .../jabref/model/database/BibDatabase.java | 15 +- .../model/entry/field/StandardField.java | 1 + src/main/resources/l10n/JabRef_en.properties | 2 +- .../CitationsRelationsTabViewModelTest.java | 131 ++++++++++ .../OpenOfficeDocumentCreatorTest.java | 1 - ...ficeCalcExportFormatContentSingleEntry.xml | 245 +++++++++--------- 17 files changed, 437 insertions(+), 189 deletions(-) create mode 100644 src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java create mode 100644 src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ede9ae9a686..8506cd4d882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- When importing entries form the "Citation relations" tab, the field [cites](https://docs.jabref.org/advanced/entryeditor/entrylinks) is now filled according to the relationship between the entries. [#10572](https://github.com/JabRef/jabref/pull/10752) + ### Changed - The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) +- We changed the order of the lists in the "Citation relations" tab. `Cites` are now on the left and `Cited by` on the right [#10572](https://github.com/JabRef/jabref/pull/10752) ### Fixed diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 7ee6f1b6968..802c04194b4 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -285,7 +285,7 @@ private List createTabs() { entryEditorTabs.add(new FileAnnotationTab(libraryTab.getAnnotationCache())); entryEditorTabs.add(new RelatedArticlesTab(entryEditorPreferences, preferencesService, dialogService, taskExecutor)); entryEditorTabs.add(new CitationRelationsTab(entryEditorPreferences, dialogService, databaseContext, - undoManager, stateManager, fileMonitor, preferencesService, libraryTab)); + undoManager, stateManager, fileMonitor, preferencesService, libraryTab, taskExecutor)); sourceTab = new SourceTab( databaseContext, diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java index aacbc90cc5f..0b0fe6acbd6 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java @@ -5,20 +5,7 @@ /** * Class to hold a BibEntry and a boolean value whether it is already in the current database or not. */ -public class CitationRelationItem { - private final BibEntry entry; - private final boolean isLocal; - - public CitationRelationItem(BibEntry entry, boolean isLocal) { - this.entry = entry; - this.isLocal = isLocal; - } - - public BibEntry getEntry() { - return entry; - } - - public boolean isLocal() { - return isLocal; - } +public record CitationRelationItem( + BibEntry entry, + boolean isLocal) { } 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 01e2249d1b0..6be605c61c1 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -32,10 +32,10 @@ import org.jabref.gui.entryeditor.EntryEditorTab; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher; -import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.NoSelectionModel; +import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; @@ -66,12 +66,14 @@ public class CitationRelationsTab extends EntryEditorTab { private final FileUpdateMonitor fileUpdateMonitor; private final PreferencesService preferencesService; private final LibraryTab libraryTab; + private final TaskExecutor taskExecutor; private final BibEntryRelationsRepository bibEntryRelationsRepository; + private final CitationsRelationsTabViewModel citationsRelationsTabViewModel; public CitationRelationsTab(EntryEditorPreferences preferences, DialogService dialogService, BibDatabaseContext databaseContext, UndoManager undoManager, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, - PreferencesService preferencesService, LibraryTab lTab) { + PreferencesService preferencesService, LibraryTab lTab, TaskExecutor taskExecutor) { this.preferences = preferences; this.dialogService = dialogService; this.databaseContext = databaseContext; @@ -80,11 +82,13 @@ public CitationRelationsTab(EntryEditorPreferences preferences, DialogService di this.fileUpdateMonitor = fileUpdateMonitor; this.preferencesService = preferencesService; this.libraryTab = lTab; + this.taskExecutor = taskExecutor; setText(Localization.lang("Citation relations")); setTooltip(new Tooltip(Localization.lang("Show articles related by citation"))); this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferencesService.getImporterPreferences()), new BibEntryRelationsCache()); + citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(databaseContext, preferencesService, undoManager, stateManager, dialogService, fileUpdateMonitor, Globals.TASK_EXECUTOR); } /** @@ -107,7 +111,7 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { citedByHBox.setPrefHeight(40); // Create Heading Lab - Label citingLabel = new Label(Localization.lang("Citing")); + Label citingLabel = new Label(Localization.lang("Cites")); styleLabel(citingLabel); Label citedByLabel = new Label(Localization.lang("Cited By")); styleLabel(citedByLabel); @@ -160,20 +164,19 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { refreshCitingButton.setOnMouseClicked(event -> { searchForRelations(entry, citingListView, abortCitingButton, - refreshCitingButton, CitationFetcher.SearchType.CITING, importCitingButton, citingProgress, true); + refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true); }); refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); // Create SplitPane to hold all nodes above - SplitPane container = new SplitPane(citedByVBox, citingVBox); - - styleFetchedListView(citingListView); + SplitPane container = new SplitPane(citingVBox, citedByVBox); styleFetchedListView(citedByListView); + styleFetchedListView(citingListView); searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, - CitationFetcher.SearchType.CITING, importCitingButton, citingProgress, false); + CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); @@ -193,7 +196,7 @@ private void styleFetchedListView(CheckListView listView) HBox separator = new HBox(); HBox.setHgrow(separator, Priority.SOMETIMES); - Node entryNode = BibEntryView.getEntryNode(entry.getEntry()); + Node entryNode = BibEntryView.getEntryNode(entry.entry()); HBox.setHgrow(entryNode, Priority.ALWAYS); HBox hContainer = new HBox(); hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); @@ -203,8 +206,8 @@ private void styleFetchedListView(CheckListView listView) jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in database"))); jumpTo.getStyleClass().add("addEntryButton"); jumpTo.setOnMouseClicked(event -> { - libraryTab.showAndEdit(entry.getEntry()); - libraryTab.clearAndSelect(entry.getEntry()); + libraryTab.showAndEdit(entry.entry()); + libraryTab.clearAndSelect(entry.entry()); citingTask.cancel(); citedByTask.cancel(); }); @@ -285,7 +288,7 @@ protected void bindToEntry(BibEntry entry) { * * @param entry BibEntry currently selected in Jabref Database * @param listView ListView to use - * @param abortButton Button to stop the search + * @param abortButton Button to stop the search * @param refreshButton refresh Button to use * @param searchType type of search (CITING / CITEDBY) */ @@ -305,7 +308,7 @@ private void searchForRelations(BibEntry entry, CheckListView> task; - if (searchType == CitationFetcher.SearchType.CITING) { + if (searchType == CitationFetcher.SearchType.CITES) { task = BackgroundTask.wrap(() -> { if (shouldRefresh) { bibEntryRelationsRepository.forceRefreshReferences(entry); @@ -332,18 +335,18 @@ private void searchForRelations(BibEntry entry, CheckListView prepareToSearchForRelations(abortButton, refreshButton, importButton, progress, task)) - .onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, - searchType, importButton, progress, fetchedList, observableList)) - .onFailure(exception -> { - LOGGER.error("Error while fetching citing Articles", exception); - hideNodes(abortButton, progress, importButton); - listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", - exception.getMessage()))); - - refreshButton.setVisible(true); - dialogService.notify(exception.getMessage()); - }) - .executeWith(Globals.TASK_EXECUTOR); + .onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, + searchType, importButton, progress, fetchedList, observableList)) + .onFailure(exception -> { + LOGGER.error("Error while fetching citing Articles", exception); + hideNodes(abortButton, progress, importButton); + listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", + exception.getMessage()))); + + refreshButton.setVisible(true); + dialogService.notify(exception.getMessage()); + }) + .executeWith(taskExecutor); } private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, @@ -354,7 +357,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView new CitationRelationItem(entr, false)) - .collect(Collectors.toList())); + .collect(Collectors.toList())); if (!observableList.isEmpty()) { listView.refresh(); @@ -396,19 +399,11 @@ private void showNodes(Node... nodes) { * * @param entriesToImport entries to import */ - private void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry entry) { + private void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry existingEntry) { citingTask.cancel(); citedByTask.cancel(); - List entries = entriesToImport.stream().map(CitationRelationItem::getEntry).collect(Collectors.toList()); - ImportHandler importHandler = new ImportHandler( - databaseContext, - preferencesService, - fileUpdateMonitor, - undoManager, - stateManager, - dialogService, - Globals.TASK_EXECUTOR); - importHandler.importEntries(entries); + + citationsRelationsTabViewModel.importEntries(entriesToImport, searchType, existingEntry); dialogService.notify(Localization.lang("Number of entries successfully imported") + ": " + entriesToImport.size()); } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java new file mode 100644 index 00000000000..dd53b44ef58 --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java @@ -0,0 +1,115 @@ +package org.jabref.gui.entryeditor.citationrelationtab; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.citationkeypattern.CitationKeyGenerator; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.PreferencesService; + +public class CitationsRelationsTabViewModel { + + private final BibDatabaseContext databaseContext; + private final PreferencesService preferencesService; + private final UndoManager undoManager; + private final StateManager stateManager; + private final DialogService dialogService; + private final FileUpdateMonitor fileUpdateMonitor; + private final TaskExecutor taskExecutor; + + public CitationsRelationsTabViewModel(BibDatabaseContext databaseContext, PreferencesService preferencesService, UndoManager undoManager, StateManager stateManager, DialogService dialogService, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor) { + this.databaseContext = databaseContext; + this.preferencesService = preferencesService; + this.undoManager = undoManager; + this.stateManager = stateManager; + this.dialogService = dialogService; + this.fileUpdateMonitor = fileUpdateMonitor; + this.taskExecutor = taskExecutor; + } + + public void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry existingEntry) { + List entries = entriesToImport.stream().map(CitationRelationItem::entry).toList(); + + ImportHandler importHandler = new ImportHandler( + databaseContext, + preferencesService, + fileUpdateMonitor, + undoManager, + stateManager, + dialogService, + taskExecutor); + + switch (searchType) { + case CITES -> importCites(entries, existingEntry, importHandler); + case CITED_BY -> importCitedBy(entries, existingEntry, importHandler); + } + } + + private void importCites(List entries, BibEntry existingEntry, ImportHandler importHandler) { + CitationKeyPatternPreferences citationKeyPatternPreferences = preferencesService.getCitationKeyPatternPreferences(); + CitationKeyGenerator generator = new CitationKeyGenerator(databaseContext, citationKeyPatternPreferences); + boolean generateNewKeyOnImport = preferencesService.getImporterPreferences().generateNewKeyOnImportProperty().get(); + + List citeKeys = getExistingEntriesFromCiteField(existingEntry); + citeKeys.removeIf(String::isEmpty); + for (BibEntry entryToCite : entries) { + if (generateNewKeyOnImport || entryToCite.getCitationKey().isEmpty()) { + String key = generator.generateKey(entryToCite); + entryToCite.setCitationKey(key); + addToKeyToList(citeKeys, key); + } else { + addToKeyToList(citeKeys, entryToCite.getCitationKey().get()); + } + } + existingEntry.setField(StandardField.CITES, toCommaSeparatedString(citeKeys)); + importHandler.importEntries(entries); + } + + private void importCitedBy(List entries, BibEntry existingEntry, ImportHandler importHandler) { + CitationKeyPatternPreferences citationKeyPatternPreferences = preferencesService.getCitationKeyPatternPreferences(); + CitationKeyGenerator generator = new CitationKeyGenerator(databaseContext, citationKeyPatternPreferences); + boolean generateNewKeyOnImport = preferencesService.getImporterPreferences().generateNewKeyOnImportProperty().get(); + + for (BibEntry entryThatCitesOurExistingEntry : entries) { + List existingCites = getExistingEntriesFromCiteField(entryThatCitesOurExistingEntry); + existingCites.removeIf(String::isEmpty); + String key; + if (generateNewKeyOnImport || entryThatCitesOurExistingEntry.getCitationKey().isEmpty()) { + key = generator.generateKey(entryThatCitesOurExistingEntry); + entryThatCitesOurExistingEntry.setCitationKey(key); + } else { + key = existingEntry.getCitationKey().get(); + } + addToKeyToList(existingCites, key); + entryThatCitesOurExistingEntry.setField(StandardField.CITES, toCommaSeparatedString(existingCites)); + } + + importHandler.importEntries(entries); + } + + private void addToKeyToList(List list, String key) { + if (!list.contains(key)) { + list.add(key); + } + } + + private List getExistingEntriesFromCiteField(BibEntry entry) { + return Arrays.stream(entry.getField(StandardField.CITES).orElse("").split(",")).collect(Collectors.toList()); + } + + private String toCommaSeparatedString(List citeentries) { + return String.join(",", citeentries); + } +} diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java index f23ba10a54f..539b99cc39d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java @@ -1,5 +1,8 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class AuthorResponse { private String authorId; private String name; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java index 50a3143bf6c..684285b46df 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java @@ -1,5 +1,8 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class CitationDataItem { private PaperDetails citingPaper; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java index 4b10cadce4f..1b87c7ab0bb 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java @@ -14,7 +14,7 @@ public interface CitationFetcher { * Possible search methods */ enum SearchType { - CITING("reference"), + CITES("reference"), CITED_BY("citation"); public final String label; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java index c87eb9346c2..999eb7eca2a 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java @@ -2,6 +2,9 @@ import java.util.List; +/** + * Used for GSON + */ public class CitationsResponse { private int offset; private int next; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java index 4df4c64ea6a..b9c53c355e9 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java @@ -1,13 +1,12 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class ReferenceDataItem { private PaperDetails citedPaper; public PaperDetails getCitedPaper() { return citedPaper; } - - public void setCitedPaper(PaperDetails citedPaper) { - this.citedPaper = citedPaper; - } } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java index 8f5506d9cb6..0a6ac34af07 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java @@ -2,6 +2,9 @@ import java.util.List; +/** + * Used for GSON + */ public class ReferencesResponse { private int offset; private int next; diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index d8ab4556482..d1bb17b098d 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -148,12 +148,7 @@ public Set getAllVisibleFields() { * Returns the entry with the given citation key. */ public synchronized Optional getEntryByCitationKey(String key) { - for (BibEntry entry : entries) { - if (key.equals(entry.getCitationKey().orElse(null))) { - return Optional.of(entry); - } - } - return Optional.empty(); + return entries.stream().filter(entry -> Objects.equals(entry.getCitationKey().orElse(null), key)).findFirst(); } /** @@ -402,9 +397,9 @@ public Collection getUsedStrings(Collection entries) { * references. * * @param entriesToResolve A collection of BibtexEntries in which all strings of the form - * #xxx# will be resolved against the hash map of string - * references stored in the database. - * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings. + * #xxx# will be resolved against the hash map of string + * references stored in the database. + * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings. * @return a list of bibtexentries, with all strings resolved. It is dependent on the value of inPlace whether copies are made or the given BibtexEntries are modified. */ public List resolveForStrings(Collection entriesToResolve, boolean inPlace) { @@ -548,7 +543,7 @@ public void setEpilog(String epilog) { /** * Registers a listener object (subscriber) to the internal event bus. * The following events are posted: - * + *

* - {@link EntriesAddedEvent} * - {@link EntryChangedEvent} * - {@link EntriesRemovedEvent} diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index c286ca6ea3c..2d277de57b7 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -34,6 +34,7 @@ public enum StandardField implements Field { // Comments of users are handled at {@link org.jabref.model.entry.field.UserSpecificCommentField} COMMENT("comment", FieldProperty.COMMENT, FieldProperty.MULTILINE_TEXT, FieldProperty.VERBATIM), CROSSREF("crossref", FieldProperty.SINGLE_ENTRY_LINK), + CITES("cites", FieldProperty.MULTIPLE_ENTRY_LINK), DATE("date", FieldProperty.DATE), DAY("day"), DAYFILED("dayfiled"), diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index e9edd71f8d8..3e766da2048 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2605,7 +2605,7 @@ Get\ more\ themes...=Get more themes... Add\ selected\ entries\ to\ database=Add selected entries to database The\ selected\ entry\ doesn't\ have\ a\ DOI\ linked\ to\ it.\ Lookup\ a\ DOI\ and\ try\ again.=The selected entry doesn't have a DOI linked to it. Lookup a DOI and try again. Cited\ By=Cited By -Citing=Citing +Cites=Cites No\ articles\ found=No articles found Restart\ search=Restart search Cancel\ search=Cancel search diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java new file mode 100644 index 00000000000..e3037b2edc6 --- /dev/null +++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java @@ -0,0 +1,131 @@ +package org.jabref.gui.entryeditor.citationrelationtab; + +import java.util.List; +import java.util.Optional; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.util.CurrentThreadTaskExecutor; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; +import org.jabref.logic.database.DuplicateCheck; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ImporterPreferences; +import org.jabref.logic.preferences.OwnerPreferences; +import org.jabref.logic.preferences.TimestampPreferences; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 CitationsRelationsTabViewModelTest { + private ImportHandler importHandler; + private BibDatabaseContext bibDatabaseContext; + private BibEntry testEntry; + + @Mock + private PreferencesService preferencesService; + @Mock + private DuplicateCheck duplicateCheck; + private BibEntry existingEntry; + private BibEntry firstEntryToImport; + private BibEntry secondEntryToImport; + private CitationsRelationsTabViewModel viewModel; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); + + ImporterPreferences importerPreferences = mock(ImporterPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(importerPreferences.isGenerateNewKeyOnImport()).thenReturn(false); + when(preferencesService.getImporterPreferences()).thenReturn(importerPreferences); + + when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); + when(preferencesService.getOwnerPreferences()).thenReturn(mock(OwnerPreferences.class, Answers.RETURNS_DEEP_STUBS)); + when(preferencesService.getTimestampPreferences()).thenReturn(mock(TimestampPreferences.class, Answers.RETURNS_DEEP_STUBS)); + + CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class); + GlobalCitationKeyPattern pattern = GlobalCitationKeyPattern.fromPattern("[auth][year]"); + when(citationKeyPatternPreferences.getKeyPattern()).thenReturn(pattern); + when(preferencesService.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences); + + bibDatabaseContext = new BibDatabaseContext(new BibDatabase()); + when(duplicateCheck.isDuplicate(any(), any(), any())).thenReturn(false); + + viewModel = new CitationsRelationsTabViewModel( + bibDatabaseContext, + preferencesService, + mock(UndoManager.class), + mock(StateManager.class, Answers.RETURNS_DEEP_STUBS), + mock(DialogService.class), + new DummyFileUpdateMonitor(), + new CurrentThreadTaskExecutor()); + + existingEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("Test2023") + .withField(StandardField.AUTHOR, "Test Author"); + + bibDatabaseContext.getDatabase().insertEntry(existingEntry); + + firstEntryToImport = new BibEntry(StandardEntryType.Article).withField(StandardField.AUTHOR, "First Author") + .withField(StandardField.YEAR, "2022") + .withCitationKey("FirstAuthorCitationKey2022"); + + secondEntryToImport = new BibEntry(StandardEntryType.Article).withField(StandardField.AUTHOR, "Second Author") + .withField(StandardField.YEAR, "2021") + .withCitationKey("SecondAuthorCitationKey20221"); + } + + @Test + void testExistingEntryCitesOtherPaperWithCitationKeys() { + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry); + assertEquals(Optional.of("FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } + + @Test + void testImportedEntriesWithExistingCitationKeysCiteExistingEntry() { + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITED_BY, existingEntry); + assertEquals(Optional.of("Test2023"), firstEntryToImport.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } + + @Test + void testExistingEntryCitesOtherPaperWithCitationKeysAndExistingCiteField() { + existingEntry.setField(StandardField.CITES, "Asdf1222"); + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry); + assertEquals(Optional.of("Asdf1222,FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } +} diff --git a/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java b/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java index 53c0d088a45..db1cffa4ab1 100644 --- a/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java +++ b/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java @@ -73,7 +73,6 @@ void testPerformExportForSingleEntry(@TempDir Path testFolder) throws Exception Input.Builder control = Input.from(Files.newInputStream(xmlFile)); Input.Builder test = Input.from(Files.newInputStream(contentXmlPath)); - // for debugging purposes // Path testPath = xmlFile.resolveSibling("test.xml"); // Files.copy(Files.newInputStream(contentXmlPath), testPath, StandardCopyOption.REPLACE_EXISTING); diff --git a/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml b/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml index 0594bb222be..618b78d30a2 100644 --- a/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml +++ b/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml @@ -1,12 +1,16 @@ - - + + - + - + @@ -75,6 +79,9 @@ Crossref + + Cites + Date @@ -387,372 +394,376 @@ 7 - + - + - + New York, NY, USA - + - + - + - + - + - + Tony Clear - + + + + - + - + - + - + - + - + - + - + - + - + - + http://doi.acm.org/10.1145/820127.820136 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + 0097-8418 - + - + - + SIGCSE Bull. - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + 4 - + - + - + 13--14 - + - + - + - + - + - + ACM - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Design and usability in security systems: daily life as a context of use? + Design and usability in security systems: daily life as a context of + use? - + - + - + - + - + - + - + - + 34 - + 2002 - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file