Skip to content

Add feature to merge .bib files into current bib #13320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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)
- We added support for multi-file import across different formats. [#13269](https://github.com/JabRef/jabref/issues/13269)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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_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),
Expand Down
5 changes: 5 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -177,6 +178,10 @@ private void createMenu() {

new SeparatorMenuItem(),

factory.createMenuItem(StandardActions.MERGE_BIBTEX_FILES_INTO_CURRENT_LIBRARY, new MergeBibFilesIntoCurrentBibAction(dialogService, preferences, stateManager, undoManager, fileUpdateMonitor, entryTypesManager)),

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))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package org.jabref.gui.mergebibfilesintocurrentbib;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.mergeentries.MergeEntriesAction;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.undo.NamedCompound;
import org.jabref.gui.undo.UndoableInsertEntries;
import org.jabref.gui.util.DirectoryDialogConfiguration;
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.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;

public class MergeBibFilesIntoCurrentBibAction extends SimpleCommand {
private static final Logger LOGGER = LoggerFactory.getLogger(MergeBibFilesIntoCurrentBibAction.class);

private final DialogService dialogService;
private final GuiPreferences preferences;
private final StateManager stateManager;
private final UndoManager undoManager;
private final FileUpdateMonitor fileUpdateMonitor;
private final BibEntryTypesManager entryTypesManager;

private boolean shouldMergeSameKeyEntries;
private boolean shouldMergeDuplicateEntries;

private final List<BibEntry> entriesToMerge = new ArrayList<>();
private final List<List<BibEntry>> duplicatePairsToMerge = new ArrayList<>();
private final List<List<BibEntry>> sameKeyPairsToMerge = new ArrayList<>();

public MergeBibFilesIntoCurrentBibAction(DialogService dialogService,
GuiPreferences preferences,
StateManager stateManager,
UndoManager undoManager,
FileUpdateMonitor fileUpdateMonitor,
BibEntryTypesManager entryTypesManager) {
this.dialogService = dialogService;
this.preferences = preferences;
this.stateManager = stateManager;
this.undoManager = undoManager;
this.fileUpdateMonitor = fileUpdateMonitor;
this.entryTypesManager = entryTypesManager;

this.executable.bind(needsDatabase(this.stateManager));
}

@Override
public void execute() {
Optional<Path> selectedDirectory = getDirectoryToMerge();
Optional<BibDatabaseContext> context = stateManager.getActiveDatabase();

MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences();

shouldMergeSameKeyEntries = mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntries();
shouldMergeDuplicateEntries = mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntries();

if (selectedDirectory.isPresent() && context.isPresent()) {
mergeBibFilesIntoCurrentBib(selectedDirectory.get(), context.get());
}
}

private Optional<Path> getDirectoryToMerge() {
DirectoryDialogConfiguration config = new DirectoryDialogConfiguration.Builder()
.withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory())
.build();

return dialogService.showDirectorySelectionDialog(config);
}

public void mergeBibFilesIntoCurrentBib(Path directory, BibDatabaseContext context) {
BibDatabase database = context.getDatabase();
Optional<Path> databasePath = context.getDatabasePath();
DuplicateCheck duplicateCheck = new DuplicateCheck(entryTypesManager);

entriesToMerge.clear();
sameKeyPairsToMerge.clear();
duplicatePairsToMerge.clear();

for (Path path : getAllBibFiles(directory, databasePath.orElseGet(() -> Path.of("")))) {
ParserResult result;
try {
result = OpenDatabase.loadDatabase(path, preferences.getImportFormatPreferences(), fileUpdateMonitor);
} catch (IOException e) {
LOGGER.error("Could not load file '{}': {}", path, e.getMessage(), e);
continue;
}
for (BibEntry toMergeEntry : result.getDatabase().getEntries()) {
processEntry(toMergeEntry, database, duplicateCheck);
}
}

database.insertEntries(entriesToMerge);
performMerges();

NamedCompound compound = new NamedCompound(Localization.lang("Merge BibTeX files into current library"));
compound.addEdit(new UndoableInsertEntries(database, entriesToMerge));
compound.end();
undoManager.addEdit(compound);
}

private void processEntry(BibEntry entry, BibDatabase database, DuplicateCheck duplicateCheck) {
for (BibEntry existingEntry : database.getEntries()) {
if (entry.equals(existingEntry)) {
return;
} else if (entry.getCitationKey().equals(existingEntry.getCitationKey())) {
if (shouldMergeSameKeyEntries) {
sameKeyPairsToMerge.add(List.of(entry, existingEntry));
}
return;
} else if (duplicateCheck.isDuplicate(entry, existingEntry, BibDatabaseMode.BIBTEX)) {
if (shouldMergeDuplicateEntries) {
duplicatePairsToMerge.add(List.of(entry, existingEntry));
}
return;
}
}
entriesToMerge.add(entry);
}

private void performMerges() {
for (List<BibEntry> pair : sameKeyPairsToMerge) {
mergeEntries(pair);
}
for (List<BibEntry> pair : duplicatePairsToMerge) {
mergeEntries(pair);
}
}

private void mergeEntries(List<BibEntry> entries) {
stateManager.setSelectedEntries(entries);
new MergeEntriesAction(dialogService, stateManager, undoManager, preferences).execute();
}

private List<Path> getAllBibFiles(Path directory, Path databasePath) {
if (!checkPathValidity(directory)) {
return List.of();
}
try (Stream<Path> 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.getMessage(), e);
}
return List.of();
}

private boolean checkPathValidity(Path directory) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nitpick:
"checkPathValidity" sounds like a function that doesn't return anything.
A better name would be isValidPath so that it can be used in conditionals as if(isValidPath(...)) rather than if(checkPathValidity(...))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already changed it! Thanks!

if (!Files.exists(directory)) {
dialogService.showErrorDialogAndWait(Localization.lang("Chosen folder does not exist:") + " " + directory);
return false;
}
if (!Files.isDirectory(directory)) {
dialogService.showErrorDialogAndWait(Localization.lang("Chosen path is not a folder:") + " " + directory);
return false;
}
if (!Files.isReadable(directory)) {
dialogService.showErrorDialogAndWait(Localization.lang("Chosen folder is not readable:") + " " + directory);
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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 shouldMergeSameKeyEntries() {
return this.shouldMergeSameKeyEntries.get();
}

public void setShouldMergeSameKeyEntries(boolean decision) {
this.shouldMergeSameKeyEntries.set(decision);
}

public BooleanProperty shouldMergeSameKeyEntriesProperty() {
return this.shouldMergeSameKeyEntries;
}

public boolean shouldMergeDuplicateEntries() {
return this.shouldMergeDuplicateEntries.get();
}

public void setShouldMergeDuplicateEntries(boolean decision) {
this.shouldMergeDuplicateEntries.set(decision);
}

public BooleanProperty shouldMergeDuplicateEntriesProperty() {
return this.shouldMergeDuplicateEntries;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,4 +59,6 @@ public interface GuiPreferences extends CliPreferences {
KeyBindingRepository getKeyBindingRepository();

NewEntryPreferences getNewEntryPreferences();

MergeBibFilesIntoCurrentBibPreferences getMergeBibFilesIntoCurrentBibPreferences();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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));
EasyBind.listen(mergeBibFilesIntoCurrentBibPreferences.shouldMergeSameKeyEntriesProperty(), (_, _, newValue) -> putBoolean(MERGE_SAME_KEY_ENTRIES, newValue));
EasyBind.listen(mergeBibFilesIntoCurrentBibPreferences.shouldMergeDuplicateEntriesProperty(), (_, _, newValue) -> putBoolean(MERGE_DUPLICATE_ENTRIES, newValue));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


return mergeBibFilesIntoCurrentBibPreferences;
}
// endregion

/**
* In GUI mode, we can lookup the directory better
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public class GeneralTab extends AbstractPreferenceTabView<GeneralTabViewModel> i
@FXML private CheckBox shouldAskForIncludingCrossReferences;
@FXML private CheckBox confirmHideTabBar;
@FXML private ComboBox<BibDatabaseMode> biblatexMode;
@FXML private CheckBox mergeSameKeyEntries;
@FXML private CheckBox mergeDuplicateEntries;
@FXML private CheckBox alwaysReformatBib;
@FXML private CheckBox autosaveLocalLibraries;
@FXML private Button autosaveLocalLibrariesHelp;
Expand Down Expand Up @@ -130,6 +132,9 @@ public void initialize() {
biblatexMode.itemsProperty().bind(viewModel.biblatexModeListProperty());
biblatexMode.valueProperty().bindBidirectional(viewModel.selectedBiblatexModeProperty());

mergeSameKeyEntries.selectedProperty().bindBidirectional(viewModel.mergeSameKeyEntriesProperty());
mergeDuplicateEntries.selectedProperty().bindBidirectional(viewModel.mergeDuplicateEntriesProperty());

alwaysReformatBib.selectedProperty().bindBidirectional(viewModel.alwaysReformatBibProperty());
autosaveLocalLibraries.selectedProperty().bindBidirectional(viewModel.autosaveLocalLibrariesProperty());
ActionFactory actionFactory = new ActionFactory();
Expand Down
Loading