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 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -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)

### Changed

Expand Down
6 changes: 6 additions & 0 deletions jabgui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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(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))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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;

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<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());
}
}

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

return dialogService.showDirectorySelectionDialog(config);
}

public void mergeBibFilesIntoCurrentBib(Path directory, BibDatabaseContext context) {
List<BibEntry> newEntries = new ArrayList<>();
List<BibEntry> selectedEntries;

BibDatabase database = context.getDatabase();
Optional<Path> databasePath = context.getDatabasePath();

BibEntryTypesManager entryTypesManager = new BibEntryTypesManager();
Copy link
Member

Choose a reason for hiding this comment

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

Inject

DuplicateCheck dupCheck = new DuplicateCheck(entryTypesManager);

for (Path p : getAllBibFiles(directory, databasePath.orElseGet(() -> Path.of("")))) {
Copy link
Member

Choose a reason for hiding this comment

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

Please avoid abbreviations

ParserResult result = loadDatabase(p);

for (BibEntry toMergeEntry : result.getDatabase().getEntries()) {
boolean validNewEntry = true;
for (BibEntry e : database.getEntries()) {
Copy link
Member

Choose a reason for hiding this comment

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

Please avoid abbreviations

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);
Copy link
Member

Choose a reason for hiding this comment

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

changes to the ui should only happen at the very end of the action.

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 BibTeX files into current library"));
Copy link
Member

Choose a reason for hiding this comment

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

Please avoid abbreviations

ce.addEdit(new UndoableInsertEntries(database, newEntries));
ce.end();

undoManager.addEdit(ce);
}

public List<Path> getAllBibFiles(Path directory, Path databasePath) {
try (Stream<Path> stream = Files.find(
directory,
Integer.MAX_VALUE,
(path, _) -> path.getFileName().toString().endsWith(".bib") &&
!path.equals(databasePath)
)) {
return stream.collect(Collectors.toList());
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
return stream.collect(Collectors.toList());
return stream.toList();

} catch (IOException e) {
LOGGER.error("Error finding .bib files in '{}'", directory.getFileName(), e);
}
Copy link
Member

Choose a reason for hiding this comment

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

I think the exception is thrown if the Files.find method cannot open or read the path, so I think there should be check for if the path is valid or not to have correct and helpful feedback to the user (including a message).

return List.of();
}

public ParserResult loadDatabase(Path file) {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you reuse existing methods? Looks like code duplication to me.
If not, put some comment on it.

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;
}
}
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));

return mergeBibFilesIntoCurrentBibPreferences;
}
// endregion

/**
* In GUI mode, we can lookup the directory better
*/
Expand Down
Loading
Loading