From d3480898b2313e518b4a142f03443c189ed96dc1 Mon Sep 17 00:00:00 2001 From: Raquel Rodrigues Date: Thu, 5 Jun 2025 16:28:26 +0100 Subject: [PATCH 1/9] Add feature to merge .bib files into current bib This feature allows for users to merge .bib files in a chosen directory into their current bib. If an imported entry is equal to an existent one, it is silently ignored. If it is a duplicate or has the same citation key, it can either be silently ignored or the entries are merged (users can configure their preference in the Preferences menu in the "Merge other bib files into current bib" tab). Users can also undo/redo this command. Fixes #12290 Co-authored-by: Guilherme Ribeiro Pereira --- CHANGELOG.md | 1 + jabgui/build.gradle.kts | 6 + .../jabref/gui/actions/StandardActions.java | 1 + .../java/org/jabref/gui/frame/MainMenu.java | 5 + .../MergeBibFilesIntoCurrentBibAction.java | 234 +++++++++ ...ergeBibFilesIntoCurrentBibPreferences.java | 38 ++ .../gui/preferences/GuiPreferences.java | 3 + .../gui/preferences/JabRefGuiPreferences.java | 30 ++ .../PreferencesDialogViewModel.java | 4 +- .../MergeBibFilesIntoCurrentBibTab.java | 34 ++ ...rgeBibFilesIntoCurrentBibTabViewModel.java | 40 ++ .../MergeBibFilesIntoCurrentBibTab.fxml | 14 + .../MergeBibFilesIntoCurrentBibTest.java | 451 ++++++++++++++++++ .../main/resources/l10n/JabRef_en.properties | 6 + 14 files changed, 866 insertions(+), 1 deletion(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java create mode 100644 jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java create mode 100644 jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java create mode 100644 jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java create mode 100644 jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml create mode 100644 jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d3e61a106..0ad30e4d069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a new `jabkit` command `pseudonymize` to pseudonymize the library. [#13109](https://github.com/JabRef/jabref/issues/13109) - We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129) - We added a new setting in the 'Entry Editor' preferences to hide the 'File Annotations' tab when no annotations are available. [#13143](https://github.com/JabRef/jabref/issues/13143) +- We added functionality to merge bib files in a given directory to the current bib and added a 'Merge other bib files into current bib' tab in the Preferences menu [#12290](https://github.com/JabRef/jabref/issues/12290#issuecomment-2909781975) ### Changed diff --git a/jabgui/build.gradle.kts b/jabgui/build.gradle.kts index 890ea06369f..f359e8700cc 100644 --- a/jabgui/build.gradle.kts +++ b/jabgui/build.gradle.kts @@ -150,6 +150,12 @@ dependencies { testImplementation("com.github.koppor:wiremock-slf4j-spi-shim:main-SNAPSHOT") testImplementation("com.github.javaparser:javaparser-symbol-solver-core:3.26.4") + + testImplementation("com.google.jimfs:jimfs:1.2") { + exclude(group = "com.google.auto.service") + exclude(group = "com.google.code.findbugs") + exclude(group = "org.checkerframework") + } } application { diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 647cef5e017..46764077d37 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -87,6 +87,7 @@ public enum StandardActions implements Action { REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING), MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), + MERGE_BIB_FILES_INTO_CURRENT_BIB(Localization.lang("Merge other bib files into current library...")), AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 1ab53f724ae..1d3488b8f9b 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -53,6 +53,7 @@ import org.jabref.gui.linkedfile.RedownloadMissingFilesAction; import org.jabref.gui.maintable.NewLibraryFromPdfActionOffline; import org.jabref.gui.maintable.NewLibraryFromPdfActionOnline; +import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibAction; import org.jabref.gui.mergeentries.BatchEntryMergeWithFetchedDataAction; import org.jabref.gui.mergeentries.MergeEntriesAction; import org.jabref.gui.mergeentries.MergeWithFetchedEntryAction; @@ -177,6 +178,10 @@ private void createMenu() { new SeparatorMenuItem(), + factory.createMenuItem(StandardActions.MERGE_BIB_FILES_INTO_CURRENT_BIB, new MergeBibFilesIntoCurrentBibAction(frame, dialogService, preferences, stateManager, undoManager, fileUpdateMonitor, aiService, entryTypesManager, clipBoardManager, taskExecutor)), + + new SeparatorMenuItem(), + factory.createSubMenu(StandardActions.REMOTE_DB, factory.createMenuItem(StandardActions.CONNECT_TO_SHARED_DB, new ConnectToSharedDatabaseCommand(frame, dialogService)), factory.createMenuItem(StandardActions.PULL_CHANGES_FROM_SHARED_DB, new PullChangesFromSharedAction(stateManager))), diff --git a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java new file mode 100644 index 00000000000..4fc49db36d4 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java @@ -0,0 +1,234 @@ +package org.jabref.gui.mergebibfilesintocurrentbib; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.ClipBoardManager; +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTabContainer; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.autosaveandbackup.BackupManager; +import org.jabref.gui.dialogs.BackupUIManager; +import org.jabref.gui.mergeentries.MergeEntriesAction; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.shared.SharedDatabaseUIManager; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableInsertEntries; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.UiTaskExecutor; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.database.DuplicateCheck; +import org.jabref.logic.importer.OpenDatabase; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.shared.DatabaseNotSupportedException; +import org.jabref.logic.shared.exception.InvalidDBMSConnectionPropertiesException; +import org.jabref.logic.shared.exception.NotASharedDatabaseException; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.FileUpdateMonitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.gui.actions.ActionHelper.needsDatabase; + +/** + * Perform a merge libraries (.bib files) in folder into current library action + */ +public class MergeBibFilesIntoCurrentBibAction extends SimpleCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(MergeBibFilesIntoCurrentBibAction.class); + + private final LibraryTabContainer tabContainer; + private final DialogService dialogService; + private final GuiPreferences preferences; + private final StateManager stateManager; + private final UndoManager undoManager; + private final FileUpdateMonitor fileUpdateMonitor; + private final AiService aiService; + private final BibEntryTypesManager entryTypesManager; + private final ClipBoardManager clipBoardManager; + private final TaskExecutor taskExecutor; + + private boolean shouldMergeSameKeyEntries; + private boolean shouldMergeDuplicateEntries; + + public MergeBibFilesIntoCurrentBibAction(LibraryTabContainer tabContainer, + DialogService dialogService, + GuiPreferences preferences, + StateManager stateManager, + UndoManager undoManager, + FileUpdateMonitor fileUpdateMonitor, + AiService aiService, + BibEntryTypesManager entryTypesManager, + ClipBoardManager clipBoardManager, + TaskExecutor taskExecutor) { + this.tabContainer = tabContainer; + this.dialogService = dialogService; + this.preferences = preferences; + this.stateManager = stateManager; + this.undoManager = undoManager; + this.fileUpdateMonitor = fileUpdateMonitor; + this.aiService = aiService; + this.entryTypesManager = entryTypesManager; + this.clipBoardManager = clipBoardManager; + this.taskExecutor = taskExecutor; + + this.executable.bind(needsDatabase(this.stateManager)); + } + + @Override + public void execute() { + Optional selectedDirectory = getDirectoryToMerge(); + Optional context = stateManager.getActiveDatabase(); + + MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences(); + + shouldMergeSameKeyEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries(); + shouldMergeDuplicateEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries(); + + if (selectedDirectory.isPresent() && context.isPresent()) { + mergeBibFilesIntoCurrentBib(selectedDirectory.get(), context.get()); + } + } + + public Optional getDirectoryToMerge() { + DirectoryDialogConfiguration config = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory()) + .build(); + + return dialogService.showDirectorySelectionDialog(config); + } + + public void mergeBibFilesIntoCurrentBib(Path directory, BibDatabaseContext context) { + List newEntries = new ArrayList<>(); + List selectedEntries; + + BibDatabase database = context.getDatabase(); + Optional databasePath = context.getDatabasePath(); + + BibEntryTypesManager entryTypesManager = new BibEntryTypesManager(); + DuplicateCheck dupCheck = new DuplicateCheck(entryTypesManager); + + for (Path p : getAllBibFiles(directory, databasePath.orElseGet(() -> Path.of("")))) { + ParserResult result = loadDatabase(p); + + for (BibEntry toMergeEntry : result.getDatabase().getEntries()) { + boolean validNewEntry = true; + for (BibEntry e : database.getEntries()) { + if (toMergeEntry.equals(e)) { + validNewEntry = false; + break; + } else if (toMergeEntry.getCitationKey().equals(e.getCitationKey())) { + validNewEntry = false; + + if (shouldMergeSameKeyEntries) { + selectedEntries = new ArrayList<>(); + selectedEntries.add(toMergeEntry); + selectedEntries.add(e); + stateManager.setSelectedEntries(selectedEntries); + new MergeEntriesAction(dialogService, stateManager, undoManager, preferences).execute(); + } + break; + } else if (dupCheck.isDuplicate(toMergeEntry, e, BibDatabaseMode.BIBTEX)) { + validNewEntry = false; + + if (shouldMergeDuplicateEntries) { + selectedEntries = new ArrayList<>(); + selectedEntries.add(toMergeEntry); + selectedEntries.add(e); + stateManager.setSelectedEntries(selectedEntries); + new MergeEntriesAction(dialogService, stateManager, undoManager, preferences).execute(); + } + break; + } + } + + if (validNewEntry) { + newEntries.add(toMergeEntry); + database.insertEntry(toMergeEntry); + } + } + } + NamedCompound ce = new NamedCompound(Localization.lang("Merge bib files into current bib")); + ce.addEdit(new UndoableInsertEntries(database, newEntries)); + ce.end(); + + undoManager.addEdit(ce); + } + + public List getAllBibFiles(Path directory, Path databasePath) { + try (Stream stream = Files.find( + directory, + Integer.MAX_VALUE, + (path, _) -> path.getFileName().toString().endsWith(".bib") && + !path.equals(databasePath) + )) { + return stream.collect(Collectors.toList()); + } catch (IOException e) { + LOGGER.error("Error finding .bib files in '{}'", directory.getFileName(), e); + } + return List.of(); + } + + public ParserResult loadDatabase(Path file) { + Path fileToLoad = file.toAbsolutePath(); + + preferences.getFilePreferences().setWorkingDirectory(fileToLoad.getParent()); + Path backupDir = preferences.getFilePreferences().getBackupDirectory(); + + ParserResult parserResult = null; + if (BackupManager.backupFileDiffers(fileToLoad, backupDir)) { + parserResult = BackupUIManager.showRestoreBackupDialog(dialogService, fileToLoad, preferences, fileUpdateMonitor, undoManager, stateManager) + .orElse(null); + } + + try { + if (parserResult == null) { + parserResult = OpenDatabase.loadDatabase(fileToLoad, + preferences.getImportFormatPreferences(), + fileUpdateMonitor); + } + + if (parserResult.hasWarnings()) { + String content = Localization.lang("Please check your library file for wrong syntax.") + + "\n\n" + parserResult.getErrorMessage(); + UiTaskExecutor.runInJavaFXThread(() -> + dialogService.showWarningDialogAndWait(Localization.lang("Open library error"), content)); + } + } catch (IOException e) { + parserResult = ParserResult.fromError(e); + LOGGER.error("Error opening file '{}'", fileToLoad, e); + } + + if (parserResult.getDatabase().isShared()) { + try { + new SharedDatabaseUIManager(tabContainer, dialogService, preferences, aiService, stateManager, entryTypesManager, + fileUpdateMonitor, undoManager, clipBoardManager, taskExecutor) + .openSharedDatabaseFromParserResult(parserResult); + } catch (SQLException | + DatabaseNotSupportedException | + InvalidDBMSConnectionPropertiesException | + NotASharedDatabaseException e) { + parserResult.getDatabaseContext().clearDatabasePath(); + parserResult.getDatabase().clearSharedDatabaseID(); + LOGGER.error("Connection error", e); + } + } + return parserResult; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java new file mode 100644 index 00000000000..9bbaa164f38 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java @@ -0,0 +1,38 @@ +package org.jabref.gui.mergebibfilesintocurrentbib; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +public class MergeBibFilesIntoCurrentBibPreferences { + private final BooleanProperty shouldMergeSameKeyEntries = new SimpleBooleanProperty(); + private final BooleanProperty shouldMergeDuplicateEntries = new SimpleBooleanProperty(); + + public MergeBibFilesIntoCurrentBibPreferences(boolean shouldMergeSameKeyEntries, boolean shouldMergeDuplicateEntries) { + this.shouldMergeSameKeyEntries.set(shouldMergeSameKeyEntries); + this.shouldMergeDuplicateEntries.set(shouldMergeDuplicateEntries); + } + + public boolean getShouldMergeSameKeyEntries() { + return this.shouldMergeSameKeyEntries.get(); + } + + public void setShouldMergeSameKeyEntries(boolean decision) { + this.shouldMergeSameKeyEntries.set(decision); + } + + public BooleanProperty shouldMergeSameKeyEntriesProperty() { + return this.shouldMergeSameKeyEntries; + } + + public boolean getShouldMergeDuplicateEntries() { + return this.shouldMergeDuplicateEntries.get(); + } + + public void setShouldMergeDuplicateEntries(boolean decision) { + this.shouldMergeDuplicateEntries.set(decision); + } + + public BooleanProperty shouldMergeDuplicateEntriesProperty() { + return this.shouldMergeDuplicateEntries; + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java b/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java index 3e439c75a49..bb795c3d8db 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java @@ -13,6 +13,7 @@ import org.jabref.gui.maintable.ColumnPreferences; import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.maintable.NameDisplayPreferences; +import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibPreferences; import org.jabref.gui.mergeentries.MergeDialogPreferences; import org.jabref.gui.newentry.NewEntryPreferences; import org.jabref.gui.preview.PreviewPreferences; @@ -58,4 +59,6 @@ public interface GuiPreferences extends CliPreferences { KeyBindingRepository getKeyBindingRepository(); NewEntryPreferences getNewEntryPreferences(); + + MergeBibFilesIntoCurrentBibPreferences getMergeBibFilesIntoCurrentBibPreferences(); } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java b/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java index 1c06d3b973a..0f08f71271d 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/JabRefGuiPreferences.java @@ -38,6 +38,7 @@ import org.jabref.gui.maintable.MainTableColumnModel; import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.maintable.NameDisplayPreferences; +import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibPreferences; import org.jabref.gui.mergeentries.DiffMode; import org.jabref.gui.mergeentries.MergeDialogPreferences; import org.jabref.gui.newentry.NewEntryDialogTab; @@ -234,6 +235,11 @@ public class JabRefGuiPreferences extends JabRefCliPreferences implements GuiPre private static final String CREATE_ENTRY_INTERPRET_PARSER_NAME = "latestInterpretParserName"; // endregion + // region MergeBibFilesPreferences + private static final String MERGE_SAME_KEY_ENTRIES = "mergeSameKeyEntries"; + private static final String MERGE_DUPLICATE_ENTRIES = "mergeDuplicateEntries"; + // endregion + private static JabRefGuiPreferences singleton; private EntryEditorPreferences entryEditorPreferences; @@ -255,6 +261,7 @@ public class JabRefGuiPreferences extends JabRefCliPreferences implements GuiPre private KeyBindingRepository keyBindingRepository; private CopyToPreferences copyToPreferences; private NewEntryPreferences newEntryPreferences; + private MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences; private JabRefGuiPreferences() { super(); @@ -431,6 +438,11 @@ private JabRefGuiPreferences() { defaults.put(CREATE_ENTRY_ID_FETCHER_NAME, DoiFetcher.NAME); defaults.put(CREATE_ENTRY_INTERPRET_PARSER_NAME, PlainCitationParserChoice.RULE_BASED.getLocalizedName()); // endregion + + // region MergeBibEntriesPreferences + defaults.put(MERGE_SAME_KEY_ENTRIES, true); + defaults.put(MERGE_DUPLICATE_ENTRIES, true); + // endregion } /** @@ -1300,6 +1312,24 @@ public NewEntryPreferences getNewEntryPreferences() { return newEntryPreferences; } + // region MergeBibFilesPreferences + @Override + public MergeBibFilesIntoCurrentBibPreferences getMergeBibFilesIntoCurrentBibPreferences() { + if (mergeBibFilesIntoCurrentBibPreferences != null) { + return mergeBibFilesIntoCurrentBibPreferences; + } + mergeBibFilesIntoCurrentBibPreferences = new MergeBibFilesIntoCurrentBibPreferences( + getBoolean(MERGE_SAME_KEY_ENTRIES), + getBoolean(MERGE_DUPLICATE_ENTRIES) + ); + + EasyBind.listen(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntriesProperty(), (obs, oldValue, newValue) -> putBoolean(MERGE_SAME_KEY_ENTRIES, newValue)); + EasyBind.listen(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntriesProperty(), (obs, oldValue, newValue) -> putBoolean(MERGE_DUPLICATE_ENTRIES, newValue)); + + return mergeBibFilesIntoCurrentBibPreferences; + } + // endregion + /** * In GUI mode, we can lookup the directory better */ diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java index 5836422f225..79c756c0cbe 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/PreferencesDialogViewModel.java @@ -28,6 +28,7 @@ import org.jabref.gui.preferences.journals.JournalAbbreviationsTab; import org.jabref.gui.preferences.keybindings.KeyBindingsTab; import org.jabref.gui.preferences.linkedfiles.LinkedFilesTab; +import org.jabref.gui.preferences.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibTab; import org.jabref.gui.preferences.nameformatter.NameFormatterTab; import org.jabref.gui.preferences.network.NetworkTab; import org.jabref.gui.preferences.preview.PreviewTab; @@ -85,7 +86,8 @@ public PreferencesDialogViewModel(DialogService dialogService, GuiPreferences pr new XmpPrivacyTab(), new CustomImporterTab(), new CustomExporterTab(), - new NetworkTab() + new NetworkTab(), + new MergeBibFilesIntoCurrentBibTab() ); } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java new file mode 100644 index 00000000000..a5724ab7b0d --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java @@ -0,0 +1,34 @@ +package org.jabref.gui.preferences.mergebibfilesintocurrentbib; + +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; + +import org.jabref.gui.preferences.AbstractPreferenceTabView; +import org.jabref.gui.preferences.PreferencesTab; +import org.jabref.logic.l10n.Localization; + +import com.airhacks.afterburner.views.ViewLoader; + +public class MergeBibFilesIntoCurrentBibTab extends AbstractPreferenceTabView implements PreferencesTab { + + @FXML private CheckBox mergeSameKeyEntries; + @FXML private CheckBox mergeDuplicateEntries; + + public MergeBibFilesIntoCurrentBibTab() { + ViewLoader.view(this) + .root(this) + .load(); + } + + public void initialize() { + this.viewModel = new MergeBibFilesIntoCurrentBibTabViewModel(preferences); + + mergeSameKeyEntries.selectedProperty().bindBidirectional(viewModel.mergeSameKeyEntriesProperty()); + mergeDuplicateEntries.selectedProperty().bindBidirectional(viewModel.mergeDuplicateEntriesProperty()); + } + + @Override + public String getTabName() { + return Localization.lang("Merge bib files"); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java new file mode 100644 index 00000000000..8ae9d0a7ee0 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java @@ -0,0 +1,40 @@ +package org.jabref.gui.preferences.mergebibfilesintocurrentbib; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibPreferences; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.preferences.PreferenceTabViewModel; + +public class MergeBibFilesIntoCurrentBibTabViewModel implements PreferenceTabViewModel { + + private final BooleanProperty mergeSameKeyEntriesProperty = new SimpleBooleanProperty(); + private final BooleanProperty mergeDuplicateEntriesProperty = new SimpleBooleanProperty(); + + private final MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences; + + public MergeBibFilesIntoCurrentBibTabViewModel(GuiPreferences preferences) { + this.mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences(); + } + + @Override + public void setValues() { + mergeSameKeyEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()); + mergeDuplicateEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()); + } + + @Override + public void storeSettings() { + mergeBibFilesIntoCurrentBibPreferences.setShouldMergeSameKeyEntries(mergeSameKeyEntriesProperty.getValue()); + mergeBibFilesIntoCurrentBibPreferences.setShouldMergeDuplicateEntries(mergeDuplicateEntriesProperty.getValue()); + } + + public BooleanProperty mergeSameKeyEntriesProperty() { + return this.mergeSameKeyEntriesProperty; + } + + public BooleanProperty mergeDuplicateEntriesProperty() { + return this.mergeDuplicateEntriesProperty; + } +} diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml new file mode 100644 index 00000000000..61a96d9170e --- /dev/null +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java new file mode 100644 index 00000000000..3b2d267b63c --- /dev/null +++ b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java @@ -0,0 +1,451 @@ +package org.jabref.gui.mergebibfilesintocurrentbib; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.ClipBoardManager; +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTabContainer; +import org.jabref.gui.StateManager; +import org.jabref.gui.mergeentries.MergeEntriesAction; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.util.OptionalObjectProperty; +import org.jabref.logic.FilePreferences; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.FileUpdateMonitor; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockitoAnnotations; +import org.testfx.framework.junit5.ApplicationExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; + +@ExtendWith(ApplicationExtension.class) +public class MergeBibFilesIntoCurrentBibTest { + private Path testFolder; + private Path testInnerFolder; + private Path currentDbFile; + private BibEntry expectedEntry1; + + @Mock + private LibraryTabContainer libraryTabContainer; + @Mock + private DialogService dialogService; + @Mock + private GuiPreferences preferences; + @Mock + private StateManager stateManager; + @Mock + private UndoManager undoManager; + @Mock + private FileUpdateMonitor fileUpdateMonitor; + @Mock + private AiService aiService; + @Mock + private BibEntryTypesManager bibEntryTypesManager; + @Mock + private ClipBoardManager clipboardManager; + @Mock + private TaskExecutor taskExecutor; + @Mock + private MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences; + + @BeforeEach + void setUp() throws IOException { + MockitoAnnotations.openMocks(this); + + FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); + + testFolder = fs.getPath("/test"); + Files.createDirectory(testFolder); + testInnerFolder = fs.getPath("/test/inner"); + Files.createDirectory(testInnerFolder); + Path backupDirectory = fs.getPath("/backups"); + Files.createDirectory(backupDirectory); + + Path bibFile1 = testInnerFolder.resolve("library1.bib"); + + currentDbFile = testFolder.resolve("current.bib"); + + String bibContent1 = """ + @article{test1, + author = {Foo Bar}, + title = {First Article}, + journal = {International Journal of Something}, + year = {2023} + }"""; + + Files.writeString(bibFile1, bibContent1); + + expectedEntry1 = new BibEntry(StandardEntryType.Article) + .withCitationKey("test1") + .withField(StandardField.AUTHOR, "Foo Bar") + .withField(StandardField.TITLE, "First Article") + .withField(StandardField.JOURNAL, "International Journal of Something") + .withField(StandardField.YEAR, "2023"); + + FilePreferences filePreferences = mock(FilePreferences.class); + when(filePreferences.getWorkingDirectory()).thenReturn(testFolder); + when(filePreferences.getBackupDirectory()).thenReturn(backupDirectory); + + ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(importFormatPreferences.bibEntryPreferences().getKeywordSeparator()).thenReturn(','); + + when(preferences.getFilePreferences()).thenReturn(filePreferences); + when(preferences.getImportFormatPreferences()).thenReturn(importFormatPreferences); + + when(stateManager.activeDatabaseProperty()).thenReturn(OptionalObjectProperty.empty()); + + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(true); + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(true); + when(preferences.getMergeBibFilesIntoCurrentBibPreferences()).thenReturn(mergeBibFilesIntoCurrentBibPreferences); + } + + @Test + public void simpleMergeTest() { + BibDatabase currentDatabase = new BibDatabase(); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + + List entries = new ArrayList<>(currentDatabase.getEntries()); + assertEquals(1, entries.size(), "Should have merged 1 entry"); + + BibEntry entry1 = entries.stream() + .filter(e -> "test1".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test1' not found")); + + assertEquals(entry1, expectedEntry1, "test1 does not match after merge"); + } + + @Test + public void equalEntriesMergeTest() { + BibDatabase currentDatabase = new BibDatabase(); + currentDatabase.insertEntry(expectedEntry1); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + + List entries = new ArrayList<>(currentDatabase.getEntries()); + assertEquals(1, entries.size(), "Should not have merged any entry"); + + BibEntry entry1 = entries.stream() + .filter(e -> "test1".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test1' not found")); + + assertEquals(entry1, expectedEntry1, "test1 does not match after merge"); + } + + @Test + public void sameCitationKeyMergeTest() { + BibEntry currentEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test1") + .withField(StandardField.AUTHOR, "Different Author") + .withField(StandardField.TITLE, "Different Title") + .withField(StandardField.JOURNAL, "Different Journal") + .withField(StandardField.YEAR, "2025"); + + BibDatabase currentDatabase = new BibDatabase(); + currentDatabase.insertEntry(currentEntry); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + // Prevents JavaFX UI(from MergeEntriesAction) from being constructed in a non-JavaFX environment + try (MockedConstruction mockedMergeEntriesAction = mockConstruction(MergeEntriesAction.class, + (mock, _) -> doNothing().when(mock).execute())) { + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + assertEquals(1, mockedMergeEntriesAction.constructed().size(), "MergeEntriesAction was not created as expected"); + } + } + + @Test + public void sameCitationKeyNoMergePreferenceTest() { + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(false); + BibEntry currentEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test1") + .withField(StandardField.AUTHOR, "Different Author") + .withField(StandardField.TITLE, "Different Title") + .withField(StandardField.JOURNAL, "Different Journal") + .withField(StandardField.YEAR, "2025"); + + BibDatabase currentDatabase = new BibDatabase(); + currentDatabase.insertEntry(currentEntry); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + + List entries = new ArrayList<>(currentDatabase.getEntries()); + assertEquals(1, entries.size(), "Should still have one entry"); + + BibEntry entry1 = entries.stream() + .filter(e -> "test1".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test1' not found")); + + assertEquals(entry1, currentEntry, "test1 does not match after merge"); + } + + @Test + public void duplicateMergeTest() { + BibEntry currentEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("DIFFERENTCITATIONKEY") + .withField(StandardField.AUTHOR, "Foo Bar") + .withField(StandardField.TITLE, "First Article") + .withField(StandardField.JOURNAL, "International Journal of Something") + .withField(StandardField.YEAR, "2023"); + + // DuplicateCheck.compareEntriesStrictly allows for different CommentsBeforeEntry + currentEntry.setCommentsBeforeEntry("%% Very important paper.\n"); + + BibDatabase currentDatabase = new BibDatabase(); + currentDatabase.insertEntry(currentEntry); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + // Prevents JavaFX UI (from MergeEntriesAction) from being constructed in a non-JavaFX environment + try (MockedConstruction mockedMergeEntriesAction = mockConstruction(MergeEntriesAction.class, + (mock, _) -> doNothing().when(mock).execute())) { + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + assertEquals(1, mockedMergeEntriesAction.constructed().size(), "MergeEntriesAction was not created as expected"); + } + } + + @Test + public void duplicateNoMergePreferenceTest() { + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(false); + BibEntry currentEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("DIFFERENTCITATIONKEY") + .withField(StandardField.AUTHOR, "Foo Bar") + .withField(StandardField.TITLE, "First Article") + .withField(StandardField.JOURNAL, "International Journal of Something") + .withField(StandardField.YEAR, "2023"); + + // DuplicateCheck.compareEntriesStrictly allows for different CommentsBeforeEntry + currentEntry.setCommentsBeforeEntry("%% Very important paper.\n"); + + BibDatabase currentDatabase = new BibDatabase(); + currentDatabase.insertEntry(currentEntry); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); + + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + + List entries = new ArrayList<>(currentDatabase.getEntries()); + assertEquals(1, entries.size(), "Should still have one entry"); + + BibEntry entry1 = entries.stream() + .filter(e -> "DIFFERENTCITATIONKEY".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test1' not found")); + + assertEquals(entry1, currentEntry, "test1 does not match after merge"); + } + + @Test + public void multipleDirectoryMergeTest() throws IOException { + Path bibFile2 = testFolder.resolve("library2.bib"); + Path bibFile3 = testFolder.resolve("library3.bib"); + + String bibContent2 = """ + @article{test2, + author = {BlaBla}, + title = {Second Article}, + journal = {International Journal of Nothing}, + year = {2024} + }"""; + + String bibContent3 = """ + @article{test3, + author = {Foo Bla}, + title = {Third Article}, + journal = {International Journal of Anything}, + year = {2025} + }"""; + + Files.writeString(bibFile2, bibContent2); + Files.writeString(bibFile3, bibContent3); + + BibEntry expectedEntry2 = new BibEntry(StandardEntryType.Article) + .withCitationKey("test2") + .withField(StandardField.AUTHOR, "BlaBla") + .withField(StandardField.TITLE, "Second Article") + .withField(StandardField.JOURNAL, "International Journal of Nothing") + .withField(StandardField.YEAR, "2024"); + + BibEntry expectedEntry3 = new BibEntry(StandardEntryType.Article) + .withCitationKey("test3") + .withField(StandardField.AUTHOR, "Foo Bla") + .withField(StandardField.TITLE, "Third Article") + .withField(StandardField.JOURNAL, "International Journal of Anything") + .withField(StandardField.YEAR, "2025"); + + BibDatabase currentDatabase = new BibDatabase(); + BibDatabaseContext currentContext = new BibDatabaseContext(currentDatabase); + currentContext.setDatabasePath(currentDbFile); + when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); + when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testFolder)); + + MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( + libraryTabContainer, + dialogService, + preferences, + stateManager, + undoManager, + fileUpdateMonitor, + aiService, + bibEntryTypesManager, + clipboardManager, + taskExecutor + ); + + action.execute(); + + List entries = new ArrayList<>(currentDatabase.getEntries()); + assertEquals(3, entries.size(), "Should have merged three entries"); + + BibEntry entry1 = entries.stream() + .filter(e -> "test1".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test1' not found")); + + assertEquals(entry1, expectedEntry1, "test1 does not match after merge"); + + BibEntry entry2 = entries.stream() + .filter(e -> "test2".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test2' not found")); + + assertEquals(entry2, expectedEntry2, "test2 does not match after merge"); + + BibEntry entry3 = entries.stream() + .filter(e -> "test3".equals(e.getCitationKey().orElse(""))) + .findFirst() + .orElseThrow(() -> new AssertionError("Entry 'test3' not found")); + + assertEquals(entry3, expectedEntry3, "test3 does not match after merge"); + } +} diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index f8a2e87a308..41a257d67fe 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2970,3 +2970,9 @@ File\ '%0'\ already\ exists.\ Use\ -f\ or\ --force\ to\ overwrite.=File '%0' alr Pseudonymizing\ library\ '%0'...=Pseudonymizing library '%0'... Invalid\ output\ file\ type\ provided.=Invalid output file type provided. Saved\ %0.=Saved %0. + +# Merge Bib Files Into Current Bib Command +Merge\ bib\ files\ into\ current\ bib=Merge bib files into current bib +Merge\ other\ bib\ files\ into\ current\ library...=Merge other bib files into current library... +Merge\ entries\ with\ same\ citation\ key=Merge entries with same citation key +Merge\ duplicate\ entries=Merge duplicate entries From e4156520c81e28f7887f20cbba3f3dd1707f39a8 Mon Sep 17 00:00:00 2001 From: Guilherme Ribeiro Pereira Date: Sun, 15 Jun 2025 12:26:37 +0100 Subject: [PATCH 2/9] fix: cleanup MergeBibFilesIntoCurrentBib - Replace 'bib files' to standardized 'BibTeX' in StandardActions - Remove redundant Javadoc in MergeBibFilesIntoCurrentBibAction - Rename boolean getters - Delete obsolete test comments - Correct Changelog issue link --- CHANGELOG.md | 2 +- .../org/jabref/gui/actions/StandardActions.java | 2 +- .../MergeBibFilesIntoCurrentBibAction.java | 7 ++----- .../MergeBibFilesIntoCurrentBibPreferences.java | 4 ++-- .../MergeBibFilesIntoCurrentBibTabViewModel.java | 6 +++--- .../MergeBibFilesIntoCurrentBibTest.java | 14 ++++++-------- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad30e4d069..1739320c53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a new `jabkit` command `pseudonymize` to pseudonymize the library. [#13109](https://github.com/JabRef/jabref/issues/13109) - We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129) - We added a new setting in the 'Entry Editor' preferences to hide the 'File Annotations' tab when no annotations are available. [#13143](https://github.com/JabRef/jabref/issues/13143) -- We added functionality to merge bib files in a given directory to the current bib and added a 'Merge other bib files into current bib' tab in the Preferences menu [#12290](https://github.com/JabRef/jabref/issues/12290#issuecomment-2909781975) +- We added functionality to merge bib files in a given directory to the current bib and added a 'Merge other bib files into current bib' tab in the Preferences menu [#12290](https://github.com/JabRef/jabref/issues/12290) ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 46764077d37..6c5c8e63d62 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -87,7 +87,7 @@ public enum StandardActions implements Action { REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING), MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), - MERGE_BIB_FILES_INTO_CURRENT_BIB(Localization.lang("Merge other bib files into current library...")), + MERGE_BIB_FILES_INTO_CURRENT_BIB(Localization.lang("Merge other BibTeX files into current library...")), AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), diff --git a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java index 4fc49db36d4..6edd10b7801 100644 --- a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java +++ b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java @@ -47,9 +47,6 @@ import static org.jabref.gui.actions.ActionHelper.needsDatabase; -/** - * Perform a merge libraries (.bib files) in folder into current library action - */ public class MergeBibFilesIntoCurrentBibAction extends SimpleCommand { private static final Logger LOGGER = LoggerFactory.getLogger(MergeBibFilesIntoCurrentBibAction.class); @@ -98,8 +95,8 @@ public void execute() { MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences(); - shouldMergeSameKeyEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries(); - shouldMergeDuplicateEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries(); + shouldMergeSameKeyEntries = mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries(); + shouldMergeDuplicateEntries = mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries(); if (selectedDirectory.isPresent() && context.isPresent()) { mergeBibFilesIntoCurrentBib(selectedDirectory.get(), context.get()); diff --git a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java index 9bbaa164f38..2c42ad8dee7 100644 --- a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java +++ b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibPreferences.java @@ -12,7 +12,7 @@ public MergeBibFilesIntoCurrentBibPreferences(boolean shouldMergeSameKeyEntries, this.shouldMergeDuplicateEntries.set(shouldMergeDuplicateEntries); } - public boolean getShouldMergeSameKeyEntries() { + public boolean shouldMergeSameKeyEntries() { return this.shouldMergeSameKeyEntries.get(); } @@ -24,7 +24,7 @@ public BooleanProperty shouldMergeSameKeyEntriesProperty() { return this.shouldMergeSameKeyEntries; } - public boolean getShouldMergeDuplicateEntries() { + public boolean shouldMergeDuplicateEntries() { return this.shouldMergeDuplicateEntries.get(); } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java index 8ae9d0a7ee0..f91382b437c 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTabViewModel.java @@ -15,13 +15,13 @@ public class MergeBibFilesIntoCurrentBibTabViewModel implements PreferenceTabVie private final MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences; public MergeBibFilesIntoCurrentBibTabViewModel(GuiPreferences preferences) { - this.mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences(); + mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences(); } @Override public void setValues() { - mergeSameKeyEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()); - mergeDuplicateEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()); + mergeSameKeyEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries()); + mergeDuplicateEntriesProperty.setValue(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries()); } @Override diff --git a/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java index 3b2d267b63c..c41b2e8dac9 100644 --- a/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java +++ b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java @@ -123,8 +123,8 @@ void setUp() throws IOException { when(stateManager.activeDatabaseProperty()).thenReturn(OptionalObjectProperty.empty()); - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(true); - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(true); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries()).thenReturn(true); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries()).thenReturn(true); when(preferences.getMergeBibFilesIntoCurrentBibPreferences()).thenReturn(mergeBibFilesIntoCurrentBibPreferences); } @@ -236,8 +236,8 @@ public void sameCitationKeyMergeTest() { @Test public void sameCitationKeyNoMergePreferenceTest() { - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(false); - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries()).thenReturn(false); BibEntry currentEntry = new BibEntry(StandardEntryType.Article) .withCitationKey("test1") .withField(StandardField.AUTHOR, "Different Author") @@ -287,7 +287,6 @@ public void duplicateMergeTest() { .withField(StandardField.JOURNAL, "International Journal of Something") .withField(StandardField.YEAR, "2023"); - // DuplicateCheck.compareEntriesStrictly allows for different CommentsBeforeEntry currentEntry.setCommentsBeforeEntry("%% Very important paper.\n"); BibDatabase currentDatabase = new BibDatabase(); @@ -320,8 +319,8 @@ public void duplicateMergeTest() { @Test public void duplicateNoMergePreferenceTest() { - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries()).thenReturn(false); - when(mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries()).thenReturn(false); + when(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries()).thenReturn(false); BibEntry currentEntry = new BibEntry(StandardEntryType.Article) .withCitationKey("DIFFERENTCITATIONKEY") .withField(StandardField.AUTHOR, "Foo Bar") @@ -329,7 +328,6 @@ public void duplicateNoMergePreferenceTest() { .withField(StandardField.JOURNAL, "International Journal of Something") .withField(StandardField.YEAR, "2023"); - // DuplicateCheck.compareEntriesStrictly allows for different CommentsBeforeEntry currentEntry.setCommentsBeforeEntry("%% Very important paper.\n"); BibDatabase currentDatabase = new BibDatabase(); From 984133dfa86b3ac6208f589d63fd2d7bbc1ed797 Mon Sep 17 00:00:00 2001 From: Guilherme Ribeiro Pereira Date: Sun, 15 Jun 2025 15:10:35 +0100 Subject: [PATCH 3/9] fix: cleanup MergeBibFilesIntoCurrentBib - Replace 'bib' to standardized 'BibTeX' in StandardActions - Delete obsolete test comments - Corrected MergeBibFilesIntoCurrentBib string in JabRef_en.properties --- .../src/main/java/org/jabref/gui/actions/StandardActions.java | 2 +- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java | 2 +- .../MergeBibFilesIntoCurrentBibTest.java | 2 -- jablib/src/main/resources/l10n/JabRef_en.properties | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 6c5c8e63d62..b32d8f1ce90 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -87,7 +87,7 @@ public enum StandardActions implements Action { REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING), MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), - MERGE_BIB_FILES_INTO_CURRENT_BIB(Localization.lang("Merge other BibTeX files into current library...")), + MERGE_BIBTEX_FILES_INTO_CURRENT_LIBRARY(Localization.lang("Merge other BibTeX files into current library...")), AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 1d3488b8f9b..ae5871d0da1 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -178,7 +178,7 @@ private void createMenu() { new SeparatorMenuItem(), - factory.createMenuItem(StandardActions.MERGE_BIB_FILES_INTO_CURRENT_BIB, new MergeBibFilesIntoCurrentBibAction(frame, dialogService, preferences, stateManager, undoManager, fileUpdateMonitor, aiService, entryTypesManager, clipBoardManager, taskExecutor)), + factory.createMenuItem(StandardActions.MERGE_BIBTEX_FILES_INTO_CURRENT_LIBRARY, new MergeBibFilesIntoCurrentBibAction(frame, dialogService, preferences, stateManager, undoManager, fileUpdateMonitor, aiService, entryTypesManager, clipBoardManager, taskExecutor)), new SeparatorMenuItem(), diff --git a/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java index c41b2e8dac9..39c34aef827 100644 --- a/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java +++ b/jabgui/src/test/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTest.java @@ -213,7 +213,6 @@ public void sameCitationKeyMergeTest() { when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); - // Prevents JavaFX UI(from MergeEntriesAction) from being constructed in a non-JavaFX environment try (MockedConstruction mockedMergeEntriesAction = mockConstruction(MergeEntriesAction.class, (mock, _) -> doNothing().when(mock).execute())) { MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( @@ -296,7 +295,6 @@ public void duplicateMergeTest() { when(stateManager.getActiveDatabase()).thenReturn(Optional.of(currentContext)); when(dialogService.showDirectorySelectionDialog(any())).thenReturn(Optional.of(testInnerFolder)); - // Prevents JavaFX UI (from MergeEntriesAction) from being constructed in a non-JavaFX environment try (MockedConstruction mockedMergeEntriesAction = mockConstruction(MergeEntriesAction.class, (mock, _) -> doNothing().when(mock).execute())) { MergeBibFilesIntoCurrentBibAction action = new MergeBibFilesIntoCurrentBibAction( diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 41a257d67fe..b282eff3a8a 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2973,6 +2973,6 @@ Saved\ %0.=Saved %0. # Merge Bib Files Into Current Bib Command Merge\ bib\ files\ into\ current\ bib=Merge bib files into current bib -Merge\ other\ bib\ files\ into\ current\ library...=Merge other bib files into current library... +Merge\ other\ BibTeX\ files\ into\ current\ library...=Merge other BibTeX files into current library... Merge\ entries\ with\ same\ citation\ key=Merge entries with same citation key Merge\ duplicate\ entries=Merge duplicate entries From 4987209ec7abd9b9245688696642cbd0b91b2f03 Mon Sep 17 00:00:00 2001 From: Raquel Rodrigues Date: Sun, 15 Jun 2025 16:10:00 +0100 Subject: [PATCH 4/9] fix: cleanup MergeBibFilesIntoCurrentBib - Replace 'bib' to standardized 'BibTeX' in all strings related to the merge libraries action - Corrected jabRef_en.properties according to the changes above --- .../main/java/org/jabref/gui/actions/StandardActions.java | 2 +- .../MergeBibFilesIntoCurrentBibAction.java | 2 +- .../MergeBibFilesIntoCurrentBibTab.java | 2 +- .../MergeBibFilesIntoCurrentBibTab.fxml | 2 +- jablib/src/main/resources/l10n/JabRef_en.properties | 5 ++--- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index b32d8f1ce90..f76edc3987a 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -87,7 +87,7 @@ public enum StandardActions implements Action { REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING), MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), - MERGE_BIBTEX_FILES_INTO_CURRENT_LIBRARY(Localization.lang("Merge other BibTeX files into current library...")), + MERGE_BIBTEX_FILES_INTO_CURRENT_LIBRARY(Localization.lang("Merge BibTeX files into current library")), AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), diff --git a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java index 6edd10b7801..07bdd9ea504 100644 --- a/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java +++ b/jabgui/src/main/java/org/jabref/gui/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibAction.java @@ -161,7 +161,7 @@ public void mergeBibFilesIntoCurrentBib(Path directory, BibDatabaseContext conte } } } - NamedCompound ce = new NamedCompound(Localization.lang("Merge bib files into current bib")); + NamedCompound ce = new NamedCompound(Localization.lang("Merge BibTeX files into current library")); ce.addEdit(new UndoableInsertEntries(database, newEntries)); ce.end(); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java index a5724ab7b0d..86c5a8d630c 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.java @@ -29,6 +29,6 @@ public void initialize() { @Override public String getTabName() { - return Localization.lang("Merge bib files"); + return Localization.lang("Merge BibTeX files into current library"); } } diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml index 61a96d9170e..6aa3595fb95 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/mergebibfilesintocurrentbib/MergeBibFilesIntoCurrentBibTab.fxml @@ -6,7 +6,7 @@ -