Skip to content

Commit 43bcbb0

Browse files
committed
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 <guilherme.ribeiro.pereira@tecnico.ulisboa.pt>
1 parent 251b84a commit 43bcbb0

File tree

14 files changed

+866
-1
lines changed

14 files changed

+866
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
2828
- We added support for import of a Refer/BibIX file format. [#13069](https://github.com/JabRef/jabref/issues/13069)
2929
- We added a new `jabkit` command `pseudonymize` to pseudonymize the library. [#13109](https://github.com/JabRef/jabref/issues/13109)
3030
- We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129)
31+
- 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)
3132

3233
### Changed
3334

jabgui/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ dependencies {
141141
testImplementation("org.wiremock:wiremock-standalone:3.12.1")
142142

143143
testImplementation("com.github.javaparser:javaparser-symbol-solver-core:3.26.4")
144+
145+
testImplementation("com.google.jimfs:jimfs:1.2") {
146+
exclude(group = "com.google.auto.service")
147+
exclude(group = "com.google.code.findbugs")
148+
exclude(group = "org.checkerframework")
149+
}
144150
}
145151

146152
application {

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public enum StandardActions implements Action {
8686
REPLACE_ALL(Localization.lang("Find and replace"), KeyBinding.REPLACE_STRING),
8787
MANAGE_KEYWORDS(Localization.lang("Manage keywords")),
8888
MASS_SET_FIELDS(Localization.lang("Manage field names & content")),
89+
MERGE_BIB_FILES_INTO_CURRENT_BIB(Localization.lang("Merge other bib files into current library...")),
8990

9091
AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")),
9192
TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE),

jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.jabref.gui.linkedfile.RedownloadMissingFilesAction;
5454
import org.jabref.gui.maintable.NewLibraryFromPdfActionOffline;
5555
import org.jabref.gui.maintable.NewLibraryFromPdfActionOnline;
56+
import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibAction;
5657
import org.jabref.gui.mergeentries.MergeEntriesAction;
5758
import org.jabref.gui.newentry.NewEntryDialogTab;
5859
import org.jabref.gui.preferences.GuiPreferences;
@@ -175,6 +176,10 @@ private void createMenu() {
175176

176177
new SeparatorMenuItem(),
177178

179+
factory.createMenuItem(StandardActions.MERGE_BIB_FILES_INTO_CURRENT_BIB, new MergeBibFilesIntoCurrentBibAction(frame, dialogService, preferences, stateManager, undoManager, fileUpdateMonitor, aiService, entryTypesManager, clipBoardManager, taskExecutor)),
180+
181+
new SeparatorMenuItem(),
182+
178183
factory.createSubMenu(StandardActions.REMOTE_DB,
179184
factory.createMenuItem(StandardActions.CONNECT_TO_SHARED_DB, new ConnectToSharedDatabaseCommand(frame, dialogService)),
180185
factory.createMenuItem(StandardActions.PULL_CHANGES_FROM_SHARED_DB, new PullChangesFromSharedAction(stateManager))),
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package org.jabref.gui.mergebibfilesintocurrentbib;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.sql.SQLException;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
13+
import javax.swing.undo.UndoManager;
14+
15+
import org.jabref.gui.ClipBoardManager;
16+
import org.jabref.gui.DialogService;
17+
import org.jabref.gui.LibraryTabContainer;
18+
import org.jabref.gui.StateManager;
19+
import org.jabref.gui.actions.SimpleCommand;
20+
import org.jabref.gui.autosaveandbackup.BackupManager;
21+
import org.jabref.gui.dialogs.BackupUIManager;
22+
import org.jabref.gui.mergeentries.MergeEntriesAction;
23+
import org.jabref.gui.preferences.GuiPreferences;
24+
import org.jabref.gui.shared.SharedDatabaseUIManager;
25+
import org.jabref.gui.undo.NamedCompound;
26+
import org.jabref.gui.undo.UndoableInsertEntries;
27+
import org.jabref.gui.util.DirectoryDialogConfiguration;
28+
import org.jabref.gui.util.UiTaskExecutor;
29+
import org.jabref.logic.ai.AiService;
30+
import org.jabref.logic.database.DuplicateCheck;
31+
import org.jabref.logic.importer.OpenDatabase;
32+
import org.jabref.logic.importer.ParserResult;
33+
import org.jabref.logic.l10n.Localization;
34+
import org.jabref.logic.shared.DatabaseNotSupportedException;
35+
import org.jabref.logic.shared.exception.InvalidDBMSConnectionPropertiesException;
36+
import org.jabref.logic.shared.exception.NotASharedDatabaseException;
37+
import org.jabref.logic.util.TaskExecutor;
38+
import org.jabref.model.database.BibDatabase;
39+
import org.jabref.model.database.BibDatabaseContext;
40+
import org.jabref.model.database.BibDatabaseMode;
41+
import org.jabref.model.entry.BibEntry;
42+
import org.jabref.model.entry.BibEntryTypesManager;
43+
import org.jabref.model.util.FileUpdateMonitor;
44+
45+
import org.slf4j.Logger;
46+
import org.slf4j.LoggerFactory;
47+
48+
import static org.jabref.gui.actions.ActionHelper.needsDatabase;
49+
50+
/**
51+
* Perform a merge libraries (.bib files) in folder into current library action
52+
*/
53+
public class MergeBibFilesIntoCurrentBibAction extends SimpleCommand {
54+
private static final Logger LOGGER = LoggerFactory.getLogger(MergeBibFilesIntoCurrentBibAction.class);
55+
56+
private final LibraryTabContainer tabContainer;
57+
private final DialogService dialogService;
58+
private final GuiPreferences preferences;
59+
private final StateManager stateManager;
60+
private final UndoManager undoManager;
61+
private final FileUpdateMonitor fileUpdateMonitor;
62+
private final AiService aiService;
63+
private final BibEntryTypesManager entryTypesManager;
64+
private final ClipBoardManager clipBoardManager;
65+
private final TaskExecutor taskExecutor;
66+
67+
private boolean shouldMergeSameKeyEntries;
68+
private boolean shouldMergeDuplicateEntries;
69+
70+
public MergeBibFilesIntoCurrentBibAction(LibraryTabContainer tabContainer,
71+
DialogService dialogService,
72+
GuiPreferences preferences,
73+
StateManager stateManager,
74+
UndoManager undoManager,
75+
FileUpdateMonitor fileUpdateMonitor,
76+
AiService aiService,
77+
BibEntryTypesManager entryTypesManager,
78+
ClipBoardManager clipBoardManager,
79+
TaskExecutor taskExecutor) {
80+
this.tabContainer = tabContainer;
81+
this.dialogService = dialogService;
82+
this.preferences = preferences;
83+
this.stateManager = stateManager;
84+
this.undoManager = undoManager;
85+
this.fileUpdateMonitor = fileUpdateMonitor;
86+
this.aiService = aiService;
87+
this.entryTypesManager = entryTypesManager;
88+
this.clipBoardManager = clipBoardManager;
89+
this.taskExecutor = taskExecutor;
90+
91+
this.executable.bind(needsDatabase(this.stateManager));
92+
}
93+
94+
@Override
95+
public void execute() {
96+
Optional<Path> selectedDirectory = getDirectoryToMerge();
97+
Optional<BibDatabaseContext> context = stateManager.getActiveDatabase();
98+
99+
MergeBibFilesIntoCurrentBibPreferences mergeBibFilesIntoCurrentBibPreferences = preferences.getMergeBibFilesIntoCurrentBibPreferences();
100+
101+
shouldMergeSameKeyEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeSameKeyEntries();
102+
shouldMergeDuplicateEntries = mergeBibFilesIntoCurrentBibPreferences.getShouldMergeDuplicateEntries();
103+
104+
if (selectedDirectory.isPresent() && context.isPresent()) {
105+
mergeBibFilesIntoCurrentBib(selectedDirectory.get(), context.get());
106+
}
107+
}
108+
109+
public Optional<Path> getDirectoryToMerge() {
110+
DirectoryDialogConfiguration config = new DirectoryDialogConfiguration.Builder()
111+
.withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory())
112+
.build();
113+
114+
return dialogService.showDirectorySelectionDialog(config);
115+
}
116+
117+
public void mergeBibFilesIntoCurrentBib(Path directory, BibDatabaseContext context) {
118+
List<BibEntry> newEntries = new ArrayList<>();
119+
List<BibEntry> selectedEntries;
120+
121+
BibDatabase database = context.getDatabase();
122+
Optional<Path> databasePath = context.getDatabasePath();
123+
124+
BibEntryTypesManager entryTypesManager = new BibEntryTypesManager();
125+
DuplicateCheck dupCheck = new DuplicateCheck(entryTypesManager);
126+
127+
for (Path p : getAllBibFiles(directory, databasePath.orElseGet(() -> Path.of("")))) {
128+
ParserResult result = loadDatabase(p);
129+
130+
for (BibEntry toMergeEntry : result.getDatabase().getEntries()) {
131+
boolean validNewEntry = true;
132+
for (BibEntry e : database.getEntries()) {
133+
if (toMergeEntry.equals(e)) {
134+
validNewEntry = false;
135+
break;
136+
} else if (toMergeEntry.getCitationKey().equals(e.getCitationKey())) {
137+
validNewEntry = false;
138+
139+
if (shouldMergeSameKeyEntries) {
140+
selectedEntries = new ArrayList<>();
141+
selectedEntries.add(toMergeEntry);
142+
selectedEntries.add(e);
143+
stateManager.setSelectedEntries(selectedEntries);
144+
new MergeEntriesAction(dialogService, stateManager, undoManager, preferences).execute();
145+
}
146+
break;
147+
} else if (dupCheck.isDuplicate(toMergeEntry, e, BibDatabaseMode.BIBTEX)) {
148+
validNewEntry = false;
149+
150+
if (shouldMergeDuplicateEntries) {
151+
selectedEntries = new ArrayList<>();
152+
selectedEntries.add(toMergeEntry);
153+
selectedEntries.add(e);
154+
stateManager.setSelectedEntries(selectedEntries);
155+
new MergeEntriesAction(dialogService, stateManager, undoManager, preferences).execute();
156+
}
157+
break;
158+
}
159+
}
160+
161+
if (validNewEntry) {
162+
newEntries.add(toMergeEntry);
163+
database.insertEntry(toMergeEntry);
164+
}
165+
}
166+
}
167+
NamedCompound ce = new NamedCompound(Localization.lang("Merge bib files into current bib"));
168+
ce.addEdit(new UndoableInsertEntries(database, newEntries));
169+
ce.end();
170+
171+
undoManager.addEdit(ce);
172+
}
173+
174+
public List<Path> getAllBibFiles(Path directory, Path databasePath) {
175+
try (Stream<Path> stream = Files.find(
176+
directory,
177+
Integer.MAX_VALUE,
178+
(path, _) -> path.getFileName().toString().endsWith(".bib") &&
179+
!path.equals(databasePath)
180+
)) {
181+
return stream.collect(Collectors.toList());
182+
} catch (IOException e) {
183+
LOGGER.error("Error finding .bib files in '{}'", directory.getFileName(), e);
184+
}
185+
return List.of();
186+
}
187+
188+
public ParserResult loadDatabase(Path file) {
189+
Path fileToLoad = file.toAbsolutePath();
190+
191+
preferences.getFilePreferences().setWorkingDirectory(fileToLoad.getParent());
192+
Path backupDir = preferences.getFilePreferences().getBackupDirectory();
193+
194+
ParserResult parserResult = null;
195+
if (BackupManager.backupFileDiffers(fileToLoad, backupDir)) {
196+
parserResult = BackupUIManager.showRestoreBackupDialog(dialogService, fileToLoad, preferences, fileUpdateMonitor, undoManager, stateManager)
197+
.orElse(null);
198+
}
199+
200+
try {
201+
if (parserResult == null) {
202+
parserResult = OpenDatabase.loadDatabase(fileToLoad,
203+
preferences.getImportFormatPreferences(),
204+
fileUpdateMonitor);
205+
}
206+
207+
if (parserResult.hasWarnings()) {
208+
String content = Localization.lang("Please check your library file for wrong syntax.")
209+
+ "\n\n" + parserResult.getErrorMessage();
210+
UiTaskExecutor.runInJavaFXThread(() ->
211+
dialogService.showWarningDialogAndWait(Localization.lang("Open library error"), content));
212+
}
213+
} catch (IOException e) {
214+
parserResult = ParserResult.fromError(e);
215+
LOGGER.error("Error opening file '{}'", fileToLoad, e);
216+
}
217+
218+
if (parserResult.getDatabase().isShared()) {
219+
try {
220+
new SharedDatabaseUIManager(tabContainer, dialogService, preferences, aiService, stateManager, entryTypesManager,
221+
fileUpdateMonitor, undoManager, clipBoardManager, taskExecutor)
222+
.openSharedDatabaseFromParserResult(parserResult);
223+
} catch (SQLException |
224+
DatabaseNotSupportedException |
225+
InvalidDBMSConnectionPropertiesException |
226+
NotASharedDatabaseException e) {
227+
parserResult.getDatabaseContext().clearDatabasePath();
228+
parserResult.getDatabase().clearSharedDatabaseID();
229+
LOGGER.error("Connection error", e);
230+
}
231+
}
232+
return parserResult;
233+
}
234+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.jabref.gui.mergebibfilesintocurrentbib;
2+
3+
import javafx.beans.property.BooleanProperty;
4+
import javafx.beans.property.SimpleBooleanProperty;
5+
6+
public class MergeBibFilesIntoCurrentBibPreferences {
7+
private final BooleanProperty shouldMergeSameKeyEntries = new SimpleBooleanProperty();
8+
private final BooleanProperty shouldMergeDuplicateEntries = new SimpleBooleanProperty();
9+
10+
public MergeBibFilesIntoCurrentBibPreferences(boolean shouldMergeSameKeyEntries, boolean shouldMergeDuplicateEntries) {
11+
this.shouldMergeSameKeyEntries.set(shouldMergeSameKeyEntries);
12+
this.shouldMergeDuplicateEntries.set(shouldMergeDuplicateEntries);
13+
}
14+
15+
public boolean getShouldMergeSameKeyEntries() {
16+
return this.shouldMergeSameKeyEntries.get();
17+
}
18+
19+
public void setShouldMergeSameKeyEntries(boolean decision) {
20+
this.shouldMergeSameKeyEntries.set(decision);
21+
}
22+
23+
public BooleanProperty shouldMergeSameKeyEntriesProperty() {
24+
return this.shouldMergeSameKeyEntries;
25+
}
26+
27+
public boolean getShouldMergeDuplicateEntries() {
28+
return this.shouldMergeDuplicateEntries.get();
29+
}
30+
31+
public void setShouldMergeDuplicateEntries(boolean decision) {
32+
this.shouldMergeDuplicateEntries.set(decision);
33+
}
34+
35+
public BooleanProperty shouldMergeDuplicateEntriesProperty() {
36+
return this.shouldMergeDuplicateEntries;
37+
}
38+
}

jabgui/src/main/java/org/jabref/gui/preferences/GuiPreferences.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.jabref.gui.maintable.ColumnPreferences;
1414
import org.jabref.gui.maintable.MainTablePreferences;
1515
import org.jabref.gui.maintable.NameDisplayPreferences;
16+
import org.jabref.gui.mergebibfilesintocurrentbib.MergeBibFilesIntoCurrentBibPreferences;
1617
import org.jabref.gui.mergeentries.MergeDialogPreferences;
1718
import org.jabref.gui.newentry.NewEntryPreferences;
1819
import org.jabref.gui.preview.PreviewPreferences;
@@ -58,4 +59,6 @@ public interface GuiPreferences extends CliPreferences {
5859
KeyBindingRepository getKeyBindingRepository();
5960

6061
NewEntryPreferences getNewEntryPreferences();
62+
63+
MergeBibFilesIntoCurrentBibPreferences getMergeBibFilesIntoCurrentBibPreferences();
6164
}

0 commit comments

Comments
 (0)