-
-
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 13 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 |
---|---|---|
|
@@ -37,6 +37,7 @@ public enum StandardActions implements Action { | |
OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), | ||
SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), | ||
MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/...")), | ||
MASS_GET_BIBLIOGRAPHIC_DATA(Localization.lang("Get bibliographic data from %0 (fully automated)", "DOI/ISBN/...")), | ||
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. Maybe rename |
||
ATTACH_FILE(Localization.lang("Attach file"), IconTheme.JabRefIcons.ATTACH_FILE), | ||
ATTACH_FILE_FROM_URL(Localization.lang("Attach file from URL"), IconTheme.JabRefIcons.DOWNLOAD_FILE), | ||
PRIORITY(Localization.lang("Priority"), IconTheme.JabRefIcons.PRIORITY), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,19 @@ | ||
|
||
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 before "package" |
||
package org.jabref.gui.mergeentries; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.TreeSet; | ||
|
||
import javax.swing.undo.UndoManager; | ||
|
||
import org.jabref.gui.DialogService; | ||
import org.jabref.gui.preferences.GuiPreferences; | ||
import org.jabref.gui.undo.NamedCompound; | ||
import org.jabref.gui.undo.UndoableChangeType; | ||
import org.jabref.gui.undo.UndoableFieldChange; | ||
import org.jabref.logic.importer.EntryBasedFetcher; | ||
import org.jabref.logic.importer.FetcherClientException; | ||
import org.jabref.logic.importer.FetcherServerException; | ||
import org.jabref.logic.importer.IdBasedFetcher; | ||
import org.jabref.logic.importer.ImportCleanup; | ||
import org.jabref.logic.importer.MergeEntriesHelper; | ||
import org.jabref.logic.importer.WebFetcher; | ||
import org.jabref.logic.importer.WebFetchers; | ||
import org.jabref.logic.l10n.Localization; | ||
|
@@ -28,22 +22,22 @@ | |
import org.jabref.model.database.BibDatabaseContext; | ||
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.field.StandardField; | ||
import org.jabref.model.entry.types.EntryType; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Class for fetching and merging bibliographic information | ||
*/ | ||
public class FetchAndMergeEntry { | ||
|
||
// All identifiers listed here should also appear at {@link org.jabref.logic.importer.CompositeIdFetcher#performSearchById} | ||
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 keep my comment! It is OK if AI deletes it. But then, re-add manually. |
||
public static List<Field> SUPPORTED_FIELDS = Arrays.asList(StandardField.DOI, StandardField.EPRINT, StandardField.ISBN); | ||
|
||
public static final List<Field> SUPPORTED_IDENTIFIER_FIELDS = Arrays.asList(StandardField.DOI, StandardField.EPRINT, StandardField.ISBN); | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(FetchAndMergeEntry.class); | ||
private static final String ERROR_FETCHING = "Error while fetching from %0"; | ||
private static final String MERGE_ENTRY_WITH = "Merge entry with %0 information"; | ||
private static final String UPDATED_ENTRY = "Updated entry with info from %0"; | ||
private static final String CANCELED_MERGING = "Canceled merging entries"; | ||
|
||
private final DialogService dialogService; | ||
private final UndoManager undoManager; | ||
private final BibDatabaseContext bibDatabaseContext; | ||
|
@@ -63,124 +57,112 @@ public FetchAndMergeEntry(BibDatabaseContext bibDatabaseContext, | |
} | ||
|
||
public void fetchAndMerge(BibEntry entry) { | ||
fetchAndMerge(entry, SUPPORTED_FIELDS); | ||
fetchAndMerge(entry, SUPPORTED_IDENTIFIER_FIELDS); | ||
} | ||
|
||
public void fetchAndMerge(BibEntry entry, Field field) { | ||
fetchAndMerge(entry, Collections.singletonList(field)); | ||
fetchAndMerge(entry, List.of(field)); | ||
} | ||
|
||
public void fetchAndMerge(BibEntry entry, List<Field> fields) { | ||
for (Field field : fields) { | ||
Optional<String> fieldContent = entry.getField(field); | ||
if (fieldContent.isPresent()) { | ||
Optional<IdBasedFetcher> fetcher = WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences()); | ||
if (fetcher.isPresent()) { | ||
BackgroundTask.wrap(() -> fetcher.get().performSearchById(fieldContent.get())) | ||
.onSuccess(fetchedEntry -> { | ||
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); | ||
String type = field.getDisplayName(); | ||
if (fetchedEntry.isPresent()) { | ||
cleanup.doPostCleanup(fetchedEntry.get()); | ||
showMergeDialog(entry, fetchedEntry.get(), fetcher.get()); | ||
} else { | ||
dialogService.notify(Localization.lang("Cannot get info based on given %0: %1", type, fieldContent.get())); | ||
} | ||
}) | ||
.onFailure(exception -> { | ||
LOGGER.error("Error while fetching bibliographic information", exception); | ||
if (exception instanceof FetcherClientException) { | ||
dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("No data was found for the identifier")); | ||
} else if (exception instanceof FetcherServerException) { | ||
dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Server not available")); | ||
} else { | ||
dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Error occurred %0", exception.getMessage())); | ||
} | ||
}) | ||
.executeWith(taskExecutor); | ||
} | ||
} else { | ||
dialogService.notify(Localization.lang("No %0 found", field.getDisplayName())); | ||
} | ||
} | ||
fields.forEach(field -> fetchAndMergeEntry(entry, field)); | ||
} | ||
|
||
private void fetchAndMergeEntry(BibEntry entry, Field field) { | ||
entry.getField(field) | ||
.flatMap(fieldContent -> WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences())) | ||
.ifPresent(fetcher -> executeFetchTask(fetcher, field, entry)); | ||
} | ||
|
||
private void executeFetchTask(IdBasedFetcher fetcher, Field field, BibEntry entry) { | ||
entry.getField(field).ifPresent(fieldContent -> | ||
BackgroundTask.wrap(() -> fetcher.performSearchById(fieldContent)) | ||
.onSuccess(fetchedEntry -> processFetchedEntry(fetchedEntry, entry, fetcher)) | ||
.onFailure(exception -> handleFetchException(exception, fetcher)) | ||
.executeWith(taskExecutor) | ||
); | ||
} | ||
|
||
private void processFetchedEntry(Optional<BibEntry> fetchedEntry, BibEntry originalEntry, IdBasedFetcher fetcher) { | ||
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); | ||
fetchedEntry.ifPresentOrElse( | ||
entry -> { | ||
cleanup.doPostCleanup(entry); | ||
showMergeDialog(originalEntry, entry, fetcher); | ||
}, | ||
() -> notifyNoInfo(originalEntry) | ||
); | ||
} | ||
|
||
private void notifyNoInfo(BibEntry entry) { | ||
dialogService.notify(Localization.lang("Cannot get info based on given %0: %1", | ||
entry.getType().getDisplayName(), | ||
entry.getCitationKey().orElse(""))); | ||
} | ||
|
||
private void handleFetchException(Exception exception, WebFetcher fetcher) { | ||
LOGGER.error("Error while fetching bibliographic information", exception); | ||
dialogService.showErrorDialogAndWait( | ||
Localization.lang(ERROR_FETCHING, fetcher.getName()), | ||
exception | ||
); | ||
} | ||
|
||
private void showMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { | ||
MergeEntriesDialog dialog = createMergeDialog(originalEntry, fetchedEntry, fetcher); | ||
Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(dialog) | ||
.map(EntriesMergeResult::mergedEntry); | ||
|
||
mergedEntry.ifPresentOrElse( | ||
entry -> processMergedEntry(originalEntry, entry, fetcher), | ||
() -> notifyCanceledMerge(originalEntry) | ||
); | ||
} | ||
|
||
private MergeEntriesDialog createMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { | ||
MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferences); | ||
dialog.setTitle(Localization.lang("Merge entry with %0 information", fetcher.getName())); | ||
dialog.setTitle(Localization.lang(MERGE_ENTRY_WITH, fetcher.getName())); | ||
dialog.setLeftHeaderText(Localization.lang("Original entry")); | ||
dialog.setRightHeaderText(Localization.lang("Entry from %0", fetcher.getName())); | ||
Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(dialog).map(EntriesMergeResult::mergedEntry); | ||
|
||
if (mergedEntry.isPresent()) { | ||
NamedCompound ce = new NamedCompound(Localization.lang("Merge entry with %0 information", fetcher.getName())); | ||
|
||
// Updated the original entry with the new fields | ||
Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName)); | ||
jointFields.addAll(mergedEntry.get().getFields()); | ||
Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName)); | ||
originalFields.addAll(originalEntry.getFields()); | ||
boolean edited = false; | ||
|
||
// entry type | ||
EntryType oldType = originalEntry.getType(); | ||
EntryType newType = mergedEntry.get().getType(); | ||
|
||
if (!oldType.equals(newType)) { | ||
originalEntry.setType(newType); | ||
ce.addEdit(new UndoableChangeType(originalEntry, oldType, newType)); | ||
edited = true; | ||
} | ||
|
||
// fields | ||
for (Field field : jointFields) { | ||
Optional<String> originalString = originalEntry.getField(field); | ||
Optional<String> mergedString = mergedEntry.get().getField(field); | ||
if (originalString.isEmpty() || !originalString.equals(mergedString)) { | ||
originalEntry.setField(field, mergedString.get()); // mergedString always present | ||
ce.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null), | ||
mergedString.get())); | ||
edited = true; | ||
} | ||
} | ||
|
||
// Remove fields which are not in the merged entry, unless they are internal fields | ||
for (Field field : originalFields) { | ||
if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) { | ||
Optional<String> originalString = originalEntry.getField(field); | ||
originalEntry.clearField(field); | ||
ce.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present | ||
edited = true; | ||
} | ||
} | ||
|
||
if (edited) { | ||
ce.end(); | ||
undoManager.addEdit(ce); | ||
dialogService.notify(Localization.lang("Updated entry with info from %0", fetcher.getName())); | ||
} else { | ||
dialogService.notify(Localization.lang("No information added")); | ||
} | ||
} else { | ||
dialogService.notify(Localization.lang("Canceled merging entries")); | ||
return dialog; | ||
} | ||
|
||
private void processMergedEntry(BibEntry originalEntry, BibEntry mergedEntry, WebFetcher fetcher) { | ||
NamedCompound ce = new NamedCompound(Localization.lang(MERGE_ENTRY_WITH, fetcher.getName())); | ||
MergeEntriesHelper.mergeEntries(originalEntry, mergedEntry, ce); | ||
|
||
if (ce.hasEdits()) { | ||
ce.end(); | ||
undoManager.addEdit(ce); | ||
} | ||
|
||
dialogService.notify(Localization.lang(UPDATED_ENTRY, fetcher.getName())); | ||
} | ||
|
||
private void notifyCanceledMerge(BibEntry entry) { | ||
String citationKey = entry.getCitationKey().orElse(entry.getAuthorTitleYear(40)); | ||
dialogService.notify(Localization.lang(CANCELED_MERGING) + " [" + citationKey + "]"); | ||
} | ||
|
||
public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) { | ||
BackgroundTask.wrap(() -> fetcher.performSearch(entry).stream().findFirst()) | ||
.onSuccess(fetchedEntry -> { | ||
if (fetchedEntry.isPresent()) { | ||
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); | ||
cleanup.doPostCleanup(fetchedEntry.get()); | ||
showMergeDialog(entry, fetchedEntry.get(), fetcher); | ||
} else { | ||
dialogService.notify(Localization.lang("Could not find any bibliographic information.")); | ||
} | ||
}) | ||
.onFailure(exception -> { | ||
LOGGER.error("Error while fetching entry with {} ", fetcher.getName(), exception); | ||
dialogService.showErrorDialogAndWait(Localization.lang("Error while fetching from %0", fetcher.getName()), exception); | ||
}) | ||
.onSuccess(fetchedEntry -> processFetchedEntryForEntryBasedFetcher(fetchedEntry, entry, fetcher)) | ||
.onFailure(exception -> handleFetchException(exception, fetcher)) | ||
.executeWith(taskExecutor); | ||
} | ||
|
||
private void processFetchedEntryForEntryBasedFetcher(Optional<BibEntry> fetchedEntry, BibEntry originalEntry, EntryBasedFetcher fetcher) { | ||
fetchedEntry | ||
.map(this::cleanupFetchedEntry) | ||
.ifPresentOrElse( | ||
fe -> showMergeDialog(originalEntry, fe, fetcher), | ||
() -> dialogService.notify(Localization.lang("Could not find any bibliographic information.")) | ||
); | ||
} | ||
|
||
private BibEntry cleanupFetchedEntry(BibEntry fetchedEntry) { | ||
ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); | ||
cleanup.doPostCleanup(fetchedEntry); | ||
return fetchedEntry; | ||
} | ||
} |
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.
replace "mass" by "batch". You used "batch" in the localization - and I like it.