-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Implement mass addition of bib information (Closes #372) #12025
base: main
Are you sure you want to change the base?
Changes from 36 commits
887dd35
656facf
0409856
489b512
dc138ce
0532df4
012dd9a
f8267ac
74f2473
29759a6
37cfeae
77e0d51
e6f0ad2
c5b7b4c
5b829b1
350502e
fe46c0c
8eac6a9
5d54083
93072cf
262d25d
4667124
ae4df14
5ed3d70
74a6285
8d9bdce
95e9ae8
9fdbbb4
ce1ea3e
7305f3c
9dce81a
5f24815
d17fd88
61f5a8f
86051ac
198f2d0
ace1975
74681e1
3197ef4
c04a2fa
d2566db
4d1b88d
e8645a2
ac8c59d
5172bc2
05b3c42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package org.jabref.gui.mergeentries; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.function.Supplier; | ||
|
||
import javafx.application.Platform; | ||
|
||
import org.jabref.gui.LibraryTab; | ||
import org.jabref.gui.StateManager; | ||
import org.jabref.gui.actions.ActionHelper; | ||
import org.jabref.gui.actions.SimpleCommand; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.gui.undo.NamedCompound; | ||
import org.jabref.logic.importer.fetcher.MergingIdBasedFetcher; | ||
import org.jabref.logic.l10n.Localization; | ||
import org.jabref.logic.util.BackgroundTask; | ||
import org.jabref.logic.util.NotificationService; | ||
import org.jabref.logic.util.TaskExecutor; | ||
import org.jabref.model.entry.BibEntry; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Handles the merging of multiple bibliography entries with fetched data. | ||
* This action performs background fetching and merging of entries while | ||
* providing progress updates to the user. | ||
*/ | ||
public class BatchEntryMergeWithFetchedDataAction extends SimpleCommand { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(BatchEntryMergeWithFetchedDataAction.class); | ||
|
||
private final Supplier<LibraryTab> tabSupplier; | ||
private final GuiPreferences preferences; | ||
private final NotificationService notificationService; | ||
private final TaskExecutor taskExecutor; | ||
|
||
public BatchEntryMergeWithFetchedDataAction(Supplier<LibraryTab> tabSupplier, | ||
GuiPreferences preferences, | ||
NotificationService notificationService, | ||
StateManager stateManager, | ||
TaskExecutor taskExecutor) { | ||
this.tabSupplier = tabSupplier; | ||
this.preferences = preferences; | ||
this.notificationService = notificationService; | ||
this.taskExecutor = taskExecutor; | ||
this.executable.bind(ActionHelper.needsDatabase(stateManager)); | ||
} | ||
|
||
@Override | ||
public void execute() { | ||
LibraryTab libraryTab = tabSupplier.get(); | ||
if (libraryTab == null) { | ||
LOGGER.error("Cannot perform batch merge: no library is open."); | ||
return; | ||
} | ||
|
||
List<BibEntry> libraryEntries = libraryTab.getDatabase().getEntries(); | ||
if (libraryEntries.isEmpty()) { | ||
notificationService.notify(Localization.lang("empty library")); | ||
return; | ||
} | ||
|
||
MergeContext context = new MergeContext( | ||
libraryTab, | ||
libraryEntries, | ||
new MergingIdBasedFetcher(preferences.getImportFormatPreferences()), | ||
notificationService | ||
); | ||
|
||
taskExecutor.execute(createBackgroundTask(context)); | ||
} | ||
|
||
private static BackgroundTask<List<String>> createBackgroundTask(MergeContext context) { | ||
BackgroundTask<List<String>> task = new BackgroundTask<>() { | ||
@Override | ||
public List<String> call() { | ||
return processMergeEntries(context, this); | ||
} | ||
}; | ||
|
||
task.setTitle(Localization.lang("Fetching and merging entries")); | ||
task.showToUser(true); | ||
|
||
task.onSuccess(updatedEntries -> | ||
handleSuccess(updatedEntries, context)); | ||
|
||
task.onFailure(ex -> | ||
handleFailure(ex, context.notificationService())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can chain these calls. General comment - do not do now - read on -- You can also use |
||
|
||
return task; | ||
} | ||
|
||
private static List<String> processMergeEntries(MergeContext context, BackgroundTask<?> task) { | ||
int totalEntries = context.entries().size(); | ||
|
||
for (int i = 0; i < totalEntries && !task.isCancelled(); i++) { | ||
BibEntry libraryEntry = context.entries().get(i); | ||
updateProgress(i, totalEntries, task); | ||
fetchAndMergeEntry(context, libraryEntry); | ||
} | ||
|
||
finalizeCompoundEdit(context); | ||
return context.updatedEntries(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The parameter |
||
} | ||
|
||
private static void fetchAndMergeEntry(MergeContext context, BibEntry libraryEntry) { | ||
LOGGER.debug("Processing library entry: {}", libraryEntry); | ||
|
||
context.fetcher().fetchEntry(libraryEntry) | ||
.filter(MergingIdBasedFetcher.FetcherResult::hasChanges) | ||
.ifPresent(result -> Platform.runLater(() -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is |
||
MergeEntriesHelper.mergeEntries(result.mergedEntry(), libraryEntry, context.compoundEdit()); | ||
libraryEntry.getCitationKey().ifPresent(context.updatedEntries()::add); | ||
})); | ||
} | ||
|
||
private static void finalizeCompoundEdit(MergeContext context) { | ||
if (!context.updatedEntries().isEmpty()) { | ||
Platform.runLater(() -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessary? |
||
context.compoundEdit().end(); | ||
context.libraryTab().getUndoManager().addEdit(context.compoundEdit()); | ||
}); | ||
} | ||
} | ||
|
||
private static void updateProgress(int currentIndex, int totalEntries, BackgroundTask<?> task) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please, simply create a new class inherting from |
||
Platform.runLater(() -> { | ||
task.updateMessage(Localization.lang("Processing entry %0 of %1", currentIndex + 1, totalEntries)); | ||
task.updateProgress(currentIndex, totalEntries); | ||
}); | ||
} | ||
|
||
private static void handleSuccess(List<String> updatedEntries, MergeContext context) { | ||
Platform.runLater(() -> { | ||
String message = updatedEntries.isEmpty() | ||
? Localization.lang("No updates found.") | ||
: Localization.lang("Batch update successful. %0 entries updated.", | ||
updatedEntries.size()); | ||
|
||
LOGGER.debug("Batch update completed. {}", message); | ||
context.notificationService().notify(message); | ||
}); | ||
} | ||
|
||
private static void handleFailure(Exception ex, NotificationService notificationService) { | ||
LOGGER.error("Error during fetch and merge", ex); | ||
Platform.runLater(() -> | ||
notificationService.notify( | ||
Localization.lang("Error while fetching and merging entries: %0", ex.getMessage()) | ||
) | ||
); | ||
} | ||
|
||
private record MergeContext( | ||
LibraryTab libraryTab, | ||
List<BibEntry> entries, | ||
MergingIdBasedFetcher fetcher, | ||
NamedCompound compoundEdit, | ||
List<String> updatedEntries, | ||
NotificationService notificationService | ||
) { | ||
MergeContext(LibraryTab libraryTab, List<BibEntry> entries, MergingIdBasedFetcher fetcher, NotificationService notificationService) { | ||
this( | ||
libraryTab, | ||
entries, | ||
fetcher, | ||
new NamedCompound(Localization.lang("Merge entries")), | ||
Collections.synchronizedList(new ArrayList<>()), | ||
notificationService | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package org.jabref.gui.mergeentries; | ||
|
||
import java.util.LinkedHashSet; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import org.jabref.gui.undo.NamedCompound; | ||
import org.jabref.gui.undo.UndoableChangeType; | ||
import org.jabref.gui.undo.UndoableFieldChange; | ||
import org.jabref.model.entry.BibEntry; | ||
import org.jabref.model.entry.field.Field; | ||
import org.jabref.model.entry.field.FieldFactory; | ||
import org.jabref.model.entry.types.EntryType; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Helper class for merging bibliography entries with undo support. | ||
* Source entry data is merged into the library entry, with longer field values preferred | ||
* and obsolete fields removed. | ||
*/ | ||
public final class MergeEntriesHelper { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no test for this. Please add a JUNit test (ctrl+shift+t in IntelliJ gets you started). -- Reading a unit test makes it easier to check whether the logic s correct. |
||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(MergeEntriesHelper.class); | ||
|
||
private MergeEntriesHelper() { | ||
} | ||
|
||
/** | ||
* Merges two BibEntry objects with undo support. | ||
* | ||
* @param entryFromFetcher The entry containing new information (source, from the fetcher) | ||
* @param entryFromLibrary The entry to be updated (target, from the library) | ||
* @param undoManager Compound edit to collect undo information | ||
*/ | ||
public static void mergeEntries(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompound undoManager) { | ||
LOGGER.debug("Entry from fetcher: {}", entryFromFetcher); | ||
LOGGER.debug("Entry from library: {}", entryFromLibrary); | ||
|
||
mergeEntryType(entryFromFetcher, entryFromLibrary, undoManager); | ||
mergeFields(entryFromFetcher, entryFromLibrary, undoManager); | ||
removeFieldsNotPresentInFetcher(entryFromFetcher, entryFromLibrary, undoManager); | ||
} | ||
|
||
private static void mergeEntryType(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompound undoManager) { | ||
EntryType fetcherType = entryFromFetcher.getType(); | ||
EntryType libraryType = entryFromLibrary.getType(); | ||
|
||
if (!libraryType.equals(fetcherType)) { | ||
LOGGER.debug("Updating type {} -> {}", libraryType, fetcherType); | ||
entryFromLibrary.setType(fetcherType); | ||
undoManager.addEdit(new UndoableChangeType(entryFromLibrary, libraryType, fetcherType)); | ||
} | ||
} | ||
|
||
private static void mergeFields(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompound undoManager) { | ||
Set<Field> allFields = new LinkedHashSet<>(); | ||
allFields.addAll(entryFromFetcher.getFields()); | ||
allFields.addAll(entryFromLibrary.getFields()); | ||
|
||
for (Field field : allFields) { | ||
Optional<String> fetcherValue = entryFromFetcher.getField(field); | ||
Optional<String> libraryValue = entryFromLibrary.getField(field); | ||
|
||
fetcherValue.ifPresent(newValue -> { | ||
if (shouldUpdateField(newValue, libraryValue)) { | ||
LOGGER.debug("Updating field {}: {} -> {}", field, libraryValue.orElse(null), newValue); | ||
entryFromLibrary.setField(field, newValue); | ||
undoManager.addEdit(new UndoableFieldChange(entryFromLibrary, field, libraryValue.orElse(null), newValue)); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
private static boolean shouldUpdateField(String fetcherValue, Optional<String> libraryValue) { | ||
return libraryValue.map(value -> fetcherValue.length() > value.length()) | ||
.orElse(true); | ||
} | ||
|
||
/** | ||
* Removes fields from the library entry that are not present in the fetcher entry. | ||
* This ensures the merged entry only contains fields that are considered current | ||
* according to the fetched data. | ||
*/ | ||
private static void removeFieldsNotPresentInFetcher(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompound undoManager) { | ||
Set<Field> obsoleteFields = new LinkedHashSet<>(entryFromLibrary.getFields()); | ||
obsoleteFields.removeAll(entryFromFetcher.getFields()); | ||
|
||
for (Field field : obsoleteFields) { | ||
if (FieldFactory.isInternalField(field)) { | ||
continue; | ||
} | ||
|
||
entryFromLibrary.getField(field).ifPresent(value -> { | ||
LOGGER.debug("Removing obsolete field {} with value {}", field, value); | ||
entryFromLibrary.clearField(field); | ||
undoManager.addEdit(new UndoableFieldChange(entryFromLibrary, field, value, null)); | ||
}); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No empty line at the beginning of a file (not sure why this file was touched by you) |
||
package org.jabref.gui.mergeentries; | ||
|
||
import javax.swing.undo.UndoManager; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, what I written in the last review, but I think, it was about consistency?
Haven't I written
MULTI_
be used as prefix?